Bola Idor with Mutual Tls
How Bola Idor Manifests in Mutual Tls
Mutual TLS (mTLS) ensures that both the client and the server prove their identity during the TLS handshake. When an API relies solely on mTLS for authentication, developers sometimes mistakenly treat the presence of a valid client certificate as sufficient authorization to access any resource. This creates a classic Broken Object Level Authorization (BOLA) / Insecure Direct Object Reference (IDOR) scenario: the application uses the client certificate to verify who is connecting, but then trusts user‑supplied identifiers (e.g., userId, accountNumber) without checking that they belong to the certificate holder.
Typical vulnerable code paths include:
- Extracting a resource ID from a query string or URL parameter and using it directly in a database lookup, while the handler only validates the client certificate.
- Using the certificate’s subject to populate a session object, but then allowing the client to override the session’s
userIdfield via a JSON body or header. - Logging or audit trails that record the client certificate’s DN, but the business logic still references a separate
X‑User‑Idheader supplied by the caller.
An attacker who possesses a legitimate client certificate (e.g., stolen from a legitimate user or obtained via a compromised device) can manipulate the request parameters to access objects belonging to other users. Because the transport layer already trusts the certificate, the server proceeds with the request and returns sensitive data, fulfilling the IDOR condition.
Real‑world analogues include CVE‑2020‑13944 (Apache Kafka) where mTLS authentication was bypassed by altering client‑provided identifiers, and CVE‑2021‑22986 (F5 BIG‑IP) where insufficient authorization checks after mTLS led to data leakage.
Mutual Tls-Specific Detection
Because middleBrick performs unauthenticated, black‑box scanning, it will first attempt the TLS handshake without presenting a client certificate. If the endpoint requires mTLS, the handshake fails and the scanner records a "TLS client certificate required" observation. This alone does not confirm an IDOR flaw, but it signals that the endpoint depends on transport‑level authentication.
To detect BOLA/IDOR in an mTLS‑protected API, you can supply a valid client certificate to middleBrick (via the CLI or dashboard) and let the scanner run its standard set of checks. The scanner will then:
- Complete the mTLS handshake using the provided certificate.
- Proceed with its 12 parallel security checks, including the BOLA/IDOR test that substitutes or manipulates object identifiers (e.g.,
/accounts/123→/accounts/456). - Compare responses: if the server returns data for the altered identifier without additional authorization errors, middleBrick flags a BOLA/IDOR finding with severity based on data sensitivity.
Example CLI command:
middlebrick scan https://api.example.com/v1/resources \
--cert ./client.crt \
--key ./client.key
The output will include a finding such as:
| Finding ID | Category | Severity | Description |
|---|---|---|---|
| BOLA-01 | BOLA/IDOR | High | Endpoint /v1/resources/{id} returns resource data when the {id} parameter is changed, despite valid mTLS client certificate. |
If you cannot provide a certificate, middleBrick will still report that the endpoint requires mTLS and advise obtaining a valid client credential for complete testing.
Mutual Tls-Specific Remediation
The fix is to ensure that authorization decisions are based on the identity proven by the client certificate, not on any identifier supplied by the caller. Below are language‑specific examples that demonstrate the vulnerable pattern and the corresponding remediation using the runtime certificate information.
Node.js (Express) with tls
const tls = require('tls');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
requestCert: true, // ask for client cert
ca: [fs.readFileSync('ca.crt')],
};
const server = tls.createServer(options, (socket) => {
// The socket.getPeerCertificate() provides the client cert
const cert = socket.getPeerCertificate();
const clientDN = cert.subject.CN; // or OID for email, UID, etc.
// Wrap the socket in an Express‑like request handler
const req = { clientDN, headers: {}, method: socket.method, url: socket.path };
const res = {};
// Example vulnerable route (IDOR)
app.get('/accounts/:id', (req, res) => {
const accountId = req.params.id; // <-- vulnerable: trusts user input
db.getAccount(accountId, (err, acct) => {
if (err) return res.status(500).send('Error');
// No check that accountId belongs to clientDN
return res.json(acct);
});
});
// Pass the request to Express
app(req, res, () => socket.end());
});
server.listen(8443);
Remediation: Derive the authorized account identifier from the certificate and ignore (or validate) the user‑supplied :id parameter.
app.get('/accounts/:id', (req, res) => {
const clientDN = req.clientDN;
// Map the DN to an internal user ID (could be a lookup in a trusted table)
const userId = dnToUserId[clientDN];
if (!userId) return res.status(403).send('Unknown client');
// Use the userId from the certificate, not the param
db.getAccountForUser(userId, (err, acct) => {
if (err) return res.status(500).send('Error');
return res.json(acct);
});
});
Go (net/http) with mTLS
func accountsHandler(w http.ResponseWriter, r *http.Request) {
// r.TLS.PeerCertificates contains the client cert chain
if len(r.TLS.PeerCertificates) == 0 {
http.Error(w, "client certificate required", http.StatusForbidden)
return
}
clientCert := r.TLS.PeerCertificates[0]
clientDN := clientCert.Subject.CommonName
// Vulnerable: using URL param without checking ownership
accountID := chi.URLParam(r, "id")
acct, err := db.GetAccount(accountID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// No verification that accountID belongs to clientDN
json.NewEncoder(w).Encode(acct)
}
Remediation:
func accountsHandler(w http.ResponseWriter, r *http.Request) {
if len(r.TLS.PeerCertificates) == 0 {
http.Error(w, "client certificate required", http.StatusForbidden)
return
}
clientCert := r.TLS.PeerCertificates[0]
clientDN := clientCert.Subject.CommonName
// Resolve the client DN to a user ID (trusted mapping)
userID, ok := dnToUserID[clientDN]
if !ok {
http.Error(w, "unknown client", http.StatusForbidden)
return
}
// Use the userID from the cert, ignore the URL param for authorization
acct, err := db.GetAccountForUser(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(acct)
}
Python (Flask) with SSL context
@app.route('/resources/<res_id>')
def get_resource(res_id):
# client cert info is available via request.environ
client_cert = request.environ.get('SSL_CLIENT_CERT')
if not client_cert:
abort(403, 'client certificate required')
# Vulnerable: using res_id directly
resource = db.get_resource(res_id)
return jsonify(resource)
Remediation:
def get_resource(res_id):
client_cert = request.environ.get('SSL_CLIENT_CERT')
if not client_cert:
abort(403, 'client certificate required')
# Parse the cert to obtain the subject DN
cert = crypto.load_certificate(crypto.FILETYPE_PEM, client_cert)
subject = cert.get_subject()
client_dn = subject.CN # or other OID
user_id = dn_to_user_id.get(client_dn)
if not user_id:
abort(403, 'unknown client')
# Authorize based on the certificate-derived user ID
resource = db.get_resource_for_user(user_id, res_id)
if not resource:
abort(404, 'resource not found or not authorized')
return jsonify(resource)
In each case, the remediation ensures that the authorization decision is bound to the identity verified during the mTLS handshake, eliminating the IDOR vector while preserving the mutual TLS benefits.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |