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]Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation