CoinMate.io · Report #3676308
POST /api/bitcoinWithdrawalFees returns financial data without authentication despite being documented as a USER OPERATION (private endpoint)
How it was found
Summary
The POST /api/bitcoinWithdrawalFees endpoint returns real-time Bitcoin withdrawal fee data without requiring any authentication, despite being explicitly documented as a "USER OPERATION" (private endpoint) in the official CoinMate API documentation.
This is the only private endpoint that does not enforce authentication. All other USER OPERATION endpoints (e.g., /api/balances, /api/openOrders, /api/bitcoinDepositAddresses) correctly reject unauthenticated requests with {"error": true, "errorMessage": "Invalid request"}.
Root Cause
The authentication middleware/filter on the /api/bitcoinWithdrawalFees endpoint is misconfigured, allowing the request to bypass HMAC-SHA256 signature verification. This is confirmed by the fact that all three official API client libraries (Java, TypeScript, Python) invoke this endpoint via their postPrivate() methods, which attach clientId, nonce, publicKey, and signature parameters.
Evidence from Official Documentation
From resources/doc.md (line 1230-1246):
## Bitcoin withdrawal fees [/bitcoinWithdrawalFees]
**USER OPERATION**
### POST [POST]
+ Request (application/x-www-form-urlencoded)
clientId=1038&nonce=15270794730&signature=94933BF157B9405A1C2F330902987300B3A73DE620023E1782635AAF16984729
The documentation explicitly shows authentication parameters (clientId, nonce, signature) as required for this endpoint.
Evidence from Official Client Libraries
TypeScript (CoinmateClient.ts):
async getBitcoinWithdrawalFees(): Promise<CoinmateResponse<any>> {
return this.httpClient.postPrivate('/bitcoinWithdrawalFees');
// ^^^^^^^^^^^ — treated as PRIVATE
}
Java (CoinmateClient.java):
public JsonObject getBitcoinWithdrawalFees() throws IOException {
return httpClient.postPrivate("/bitcoinWithdrawalFees", Map.of());
// ^^^^^^^^^^^ — treated as PRIVATE
}
Steps to Reproduce
Step 1: Send unauthenticated request to the vulnerable endpoint
curl -s -X POST https://coinmate.io/api/bitcoinWithdrawalFees
Response (HTTP 200 — data returned WITHOUT authentication):
{
"error": false,
"errorMessage": null,
"data": {
"low": 0.000011,
"high": 0.000019,
"timestamp": 1776286189432
}
}
Step 2: Confirm other private endpoints correctly reject unauthenticated requests
curl -s -X POST https://coinmate.io/api/balances
Response (correctly blocked):
{
"error": true,
"errorMessage": "Invalid request",
"data": null
}
Step 3: Confirm the endpoint ignores arbitrary authentication parameters
curl -s -X POST https://coinmate.io/api/bitcoinWithdrawalFees \
-d "clientId=999&publicKey=FAKE_KEY&nonce=12345&signature=AAAAAAAAAAAAAAAAAAAAAA"
Response (still returns data — authentication is completely bypassed):
{
"error": false,
"errorMessage": null,
"data": {
"low": 0.000011,
"high": 0.000019,
"timestamp": 1776286189615
}
}
This confirms the authentication middleware is not invoked at all for this endpoint.
Step 4: Confirm GET is not allowed (POST-only API endpoint)
curl -s -X GET https://coinmate.io/api/bitcoinWithdrawalFees
Response (HTTP 405):
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'GET' is not supported.",
"instance": "/api/bitcoinWithdrawalFees"
}
Authentication Enforcement Comparison
| Endpoint | Classification | Auth Required? | Unauthenticated Response |
|---|---|---|---|
| POST /api/balances | USER OPERATION | Yes | "Invalid request" |
| POST /api/openOrders | USER OPERATION | Yes | "Invalid request" |
| POST /api/traderFees | USER OPERATION | Yes | "Invalid request" |
| POST /api/bitcoinDepositAddresses | USER OPERATION | Yes | "Invalid request" |
| POST /api/transactionHistory | USER OPERATION | Yes | "Invalid request" |
| POST /api/tradeHistory | USER OPERATION | Yes | "Invalid request" |
| POST /api/orderHistory | USER OPERATION | Yes | "Invalid request" |
| POST /api/transferHistory | USER OPERATION | Yes | "Invalid request" |
| POST /api/bitcoinWithdrawalFees | USER OPERATION | No | Returns data |
The endpoint is the only one in its category that fails to enforce authentication.
Cross-Origin Exploitation
The response includes Access-Control-Allow-Origin: *, meaning any website on the internet can read this data cross-origin via JavaScript:
// Any website can execute this:
fetch('https://coinmate.io/api/bitcoinWithdrawalFees', {method: 'POST'})
.then(r => r.json())
.then(d => console.log('Fee data:', d.data));
See the attached cors_poc.html for a working demonstration.
Recommended Fix
Apply the same authentication middleware used by all other USER OPERATION endpoints to /api/bitcoinWithdrawalFees.
Impact
Security Impact
1. Broken Access Control (OWASP A01:2021):
A private endpoint designed to require HMAC-SHA256 authentication (clientId + nonce + publicKey + signature) is fully accessible without any credentials. This demonstrates a gap in the authentication enforcement layer — the endpoint was classified as USER OPERATION in the official documentation, its official client libraries (TypeScript, Java, Python) all invoke it via postPrivate(), and the documented request example includes full authentication parameters. Yet the server does not validate any of them.
2. Information Disclosure of Internal Operational Data: The endpoint exposes real-time internal fee calculation data that changes dynamically:
low: Current minimum BTC withdrawal fee (e.g., 0.000011 BTC)high: Current maximum BTC withdrawal fee (e.g., 0.000019 BTC)timestamp: Exact millisecond server-side timestamp of the last fee recalculation
This data reflects CoinMate's internal fee engine state and is not equivalent to static fee schedules published on marketing pages.
3. Cross-Origin Data Theft via CORS Misconfiguration:
The response includes Access-Control-Allow-Origin: *, which means any malicious website can silently read this data via JavaScript without the user's knowledge. A phishing page or compromised ad could harvest fee data in the background. See the attached cors_poc.html for a working demonstration.
4. Authentication Framework Inconsistency:
This is the only USER OPERATION endpoint (out of 14+ tested) that does not enforce authentication. All others (/api/balances, /api/openOrders, /api/traderFees, /api/bitcoinDepositAddresses, /api/transactionHistory, /api/tradeHistory, /api/orderHistory, /api/transferHistory) correctly reject unauthenticated requests with {"error": true, "errorMessage": "Invalid request"}. This inconsistency strongly indicates a configuration error rather than an intentional design decision, and raises the concern that other endpoints may have similar gaps that have not yet been discovered.
5. Potential for Broader Impact: If the root cause is a missing filter/interceptor registration for this specific path, the same class of misconfiguration could affect other endpoints that were recently added, refactored, or moved between public and private scopes.