HIGH bola idormutual tls

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 userId field 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‑Id header 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 IDCategorySeverityDescription
BOLA-01BOLA/IDORHighEndpoint /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 IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Does middleBrick need a client certificate to test an mTLS‑protected API for BOLA/IDOR?
middleBrick performs unauthenticated scanning by default. If the endpoint requires a client certificate, the scanner will note that mTLS is required and cannot complete the test without a valid cert. You can provide a client certificate via the CLI or dashboard to allow middleBrick to finish the handshake and run its full set of checks, including BOLA/IDOR.
What specific evidence does middleBrick look for to report a BOLA/IDOR finding on an mTLS endpoint?
After completing the mTLS handshake (with a supplied certificate), middleBrick sends requests that modify object identifiers in URLs, headers, or body parameters. If the server returns the requested object without additional authorization errors (e.g., it does not confirm that the identifier matches the certificate holder), middleBrick flags a BOLA/IDOR finding with severity based on the sensitivity of the exposed data.