Webhook Abuse with Mutual Tls
How Webhook Abuse Manifests in Mutual TLS
Mutual TLS (mTLS) guarantees that both the client and the server present valid certificates during the TLS handshake. This prevents unauthorized network‑level access, but it does not protect the application logic that processes the webhook payload after the handshake succeeds. An attacker who can obtain a valid client certificate (e.g., by stealing a certificate from a compromised partner or by abusing a mis‑issued cert) can then send arbitrary webhook requests that the server will accept and process.
Typical abuse patterns include:
- Payload‑driven SSRF: The webhook endpoint expects a URL field and later fetches that URL with server‑side privileges. Because mTLS only authenticates the caller, the attacker can supply an internal URL (e.g.,
http://169.254.169.254/latest/meta-data/) to reach cloud metadata services. - Command injection via unsanitized fields: Some webhook handlers embed user‑supplied strings into shell commands or database queries without proper escaping. A valid mTLS connection lets the attacker inject
; rm -rf /or SQL payloads. - Excessive agency: If the webhook forwards the payload to an downstream AI/LLM service (e.g., for summarization), the attacker can craft prompts that cause the model to leak system prompts or execute unwanted tool calls.
These issues appear in code paths that follow the TLS handshake, such as the Express middleware that verifies the client cert and then immediately parses the request body:
const tlsOptions = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
ca: fs.readFileSync('ca.crt'),
requestCert: true,
rejectUnauthorized: true
};
const server = https.createServer(tlsOptions, (req, res) => {
// mTLS handshake succeeded at this point
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
// ❌ No validation of the parsed JSON
const payload = JSON.parse(body);
// Example vulnerable usage
const url = payload.callback_url; // attacker‑controlled
http.get(url, (resp) => { /* ... */ }); // SSRF
});
});
server.listen(443);
Even though the connection is mutually authenticated, the lack of application‑level checks enables the abuse.
Mutual TLS‑Specific Detection
Detecting webhook abuse in an mTLS‑protected service requires probing the authenticated channel for insufficient input validation. middleBrick performs unauthenticated black‑box scans, but it can still test mTLS endpoints by presenting a valid client certificate when one is provided as part of the scan request (e.g., via the --cert and --key flags of the CLI). The scanner then sends a series of crafted webhook payloads to uncover the following:
- SSRF probes: Attempts to fetch internal addresses (
127.0.0.1,169.254.169.254, Docker daemon socket) through any URL‑like field in the payload. - Injection probes: Payloads containing SQL meta‑characters (
' OR '1'='1) or shell metacharacters (; ls -la) in fields that are later used in commands or queries. - LLM‑specific probes (if the webhook forwards to an AI service): System‑prompt extraction strings and jailbreak attempts to see whether the downstream model leaks secrets or executes unintended tool calls.
Example CLI command that includes a client certificate for mTLS:
middlebrick scan https://api.partner.com/webhook \
--cert ./client.crt \
--key ./client.key \
--ca ./ca.crt
The scanner returns a risk score (A–F) and a per‑category breakdown. Findings related to webhook abuse will appear under the Input Validation and SSRF checks, with severity ratings and remediation guidance. The same checks are available in the GitHub Action (uses: middlebrick/action@v1) and can be added to CI pipelines to fail a build when the score drops below a defined threshold.
Because middleBrick does not require agents or configuration, teams can quickly verify that a newly deployed mTLS webhook endpoint does not inadvertently trust the transport layer alone.
Mutual TLS‑Specific Remediation
Remediation focuses on adding rigorous application‑level validation while preserving the transport‑level guarantees of mTLS. The following steps are language‑agnostic but illustrated with Node.js/Express.
- Enforce strict content‑type and size limits before parsing the body.
- Validate the JSON schema** using a library such as
ajv. Define allowable fields, data types, and patterns (e.g., URLs must match an allowlist of domains). - Sanitize or avoid dangerous usage** of user‑supplied data. If a URL must be fetched, resolve it against an allowlist and disallow private IP ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16). - Use parameterized queries or ORM methods** for any data that goes into a database.
- When forwarding to an LLM, apply prompt sanitization** and limit the model’s tool scope; never forward raw user strings as system instructions.
Re‑written example with schema validation and SSRF protection:
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });
const schema = {
type: 'object',
properties: {
event: { type: 'string', enum: ['order.created', 'payment.updated'] },
callback_url: {
type: 'string',
pattern: '^https:\\/(api\\.trustedpartner\\.com|webhooks\\.example\\.org)\\/',
maxLength: 2048
},
amount: { type: 'number', minimum: 0 }
},
required: ['event', 'callback_url', 'amount'],
additionalProperties: false
};
const validate = ajv.compile(schema);
function isAllowedUrl(url) {
try {
const u = new URL(url);
// block private/reserved IP ranges
const blocked = [
/^10\\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\\.|^192\.168\\./, /^169\.254\\./
];
return !blocked.some(re => re.test(u.hostname)) &&
u.protocol === 'https:' &&
/^api\\.trustedpartner\\.com$|^webhooks\\.example\\.org$/.test(u.hostname);
} catch (_) {
return false;
}
}
const tlsOptions = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
ca: fs.readFileSync('ca.crt'),
requestCert: true,
rejectUnauthorized: true
};
const server = https.createServer(tlsOptions, (req, res) => {
if (req.method !== 'POST' || req.headers['content-type'] !== 'application/json') {
res.writeHead(415); return res.end('Unsupported Media Type');
}
let body = '';
req.on('data', chunk => { if (body.length > 1*1024*1024) req.connection.destroy(); body += chunk; });
req.on('end', () => {
let payload;
try { payload = JSON.parse(body); } catch (e) {
res.writeHead(400); return res.end('Invalid JSON');
}
if (!validate(payload)) {
res.writeHead(400); return res.end(`Invalid payload: ${ajv.errorsText(validate.errors)}`);
}
if (!isAllowedUrl(payload.callback_url)) {
res.writeHead(403); return res.end('Callback URL not allowed');
}
// Safe to use payload.callback_url in an outbound request
https.get(payload.callback_url, (resp) => {
// handle response
}).on('error', err => {
res.writeHead(502); return res.end('Bad Gateway');
});
res.writeHead(200); res.end('OK');
});
});
server.listen(443);
Key points:
- The mTLS settings (
requestCert,rejectUnauthorized) remain unchanged, preserving mutual authentication. - Application‑layer checks (schema, URL allowlist, input size) block the abuse patterns even when a valid client certificate is presented.
- Similar principles apply in other stacks: use
javax.servlet.Filterin Java,Django middlewarein Python, orASP.NET Corepolicies to enforce schema validation and outbound request filtering.
By combining transport‑level mutual TLS with rigorous input validation, organizations eliminate the classic "trust the channel, trust the data" gap that leads to webhook abuse.
Frequently Asked Questions
Does mutual TLS alone prevent webhook abuse?
How can I test my mTLS webhook endpoint for abuse using middleBrick?
middlebrick scan https://api.example.com/webhook --cert ./client.crt --key ./client.key --ca ./ca.crt. middleBrick will then send crafted payloads to detect SSRF, injection, and LLM‑specific issues, returning a risk score and detailed findings.