Webhook Abuse with Jwt Tokens
How Webhook Abuse Manifests in JWT Tokens
Webhook abuse in JWT tokens typically occurs when services accept JWTs without validating the intended audience or the source of the webhook. Attackers can exploit this by crafting JWTs that appear legitimate but target endpoints that should never process them.
The most common pattern involves JWTs with broad audience claims (aud) that are accepted by multiple services. When a webhook endpoint accepts any valid JWT without checking if the token was intended for that specific service, it creates an abuse vector. An attacker can intercept a JWT meant for Service A, then replay it to Service B's webhook endpoint, which accepts it because it validates the signature but not the intended audience.
Another manifestation occurs with JWTs containing webhook URLs in their payload. If the receiving service blindly trusts the URL in the token and makes outbound requests, attackers can craft JWTs with malicious URLs. This becomes particularly dangerous when combined with JWTs that have long expiration times or when services cache JWT validation results.
Consider this vulnerable pattern in a Node.js webhook handler:
app.post('/webhook', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).send('Missing token');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
await processWebhook(decoded.data, req.body);
res.status(200).send('OK');
} catch (err) {
res.status(401).send('Invalid token');
}
});This code verifies the JWT signature but fails to check if the webhook is intended for this specific service. An attacker can take a valid JWT from any service and replay it here.
Time-based abuse is another vector. JWTs with sliding expiration or refresh tokens can be abused if the webhook service doesn't track token usage patterns. An attacker might capture a refresh token and use it to generate multiple JWTs over an extended period, each potentially triggering webhook processing.
JWT Tokens-Specific Detection
Detecting webhook abuse in JWT tokens requires examining both the token structure and the webhook processing logic. The first indicator is missing audience validation. When scanning JWT tokens, check if the 'aud' claim exists and if the receiving service validates it against expected values.
middleBrick's JWT-specific scanning looks for several abuse patterns:
- Tokens missing audience (aud) claims when the spec requires them
- Broad audience values that match multiple services
- Webhook URLs embedded in token payloads without validation
- Excessive token lifetimes that enable prolonged abuse
- Missing token binding between the issuer and the webhook endpoint
During runtime scanning, middleBrick tests if your webhook endpoints accept JWTs from unexpected sources. It generates JWTs with manipulated audience claims and payload URLs to verify if your service blindly accepts them. The scanner also checks if your service makes outbound requests based on token contents, which is a critical abuse vector.
Code analysis for detection should include:
// Vulnerable: accepts any valid JWT
function verifyWebhookToken(token, expectedAudience) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
return decoded; // Missing audience validation!
} catch (err) {
return null;
}
}
// Secure: validates audience and binding
function verifyWebhookToken(token, expectedAudience, expectedIssuer) {
try {
const decoded = jwt.verify(token, SECRET_KEY, {
audience: expectedAudience,
issuer: expectedIssuer
});
// Additional binding check
if (decoded.webhookUrl && !isValidWebhookUrl(decoded.webhookUrl)) {
throw new Error('Invalid webhook URL');
}
return decoded;
} catch (err) {
return null;
}
}middleBrick also checks for missing rate limiting on JWT-authenticated webhook endpoints. Even with proper token validation, a valid token can be replayed repeatedly to cause a denial of service. The scanner tests if your endpoint enforces rate limits per token or per IP.
Another detection pattern involves checking for missing nonce or jti (JWT ID) validation. Without these, attackers can replay the same valid JWT multiple times to abuse your webhook processing.
JWT Tokens-Specific Remediation
| Issue | Remediation | Code Example |
|---|---|---|
| Missing audience validation | Validate 'aud' claim against expected values | |
| Untrusted webhook URLs in payload | Validate URLs against whitelist | |
| No token binding | Bind tokens to specific webhook endpoints | |
| Missing replay protection | Use jti claims with blacklist | |
Implementing proper JWT webhook security requires a defense-in-depth approach. Start with strict audience validation using the 'aud' claim. Each webhook endpoint should only accept JWTs intended for that specific service or endpoint.
For webhook URLs embedded in tokens, implement strict validation. Never trust URLs from token payloads without verification. Use a whitelist approach where only pre-approved domains or endpoints are allowed. Here's a comprehensive validation function:
function validateWebhookToken(token, options) {
const { expectedAudience, expectedIssuer, urlWhitelist } = options;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
audience: expectedAudience,
issuer: expectedIssuer,
maxAge: '15m' // Short lifetime for webhooks
});
// Validate webhook URL if present
if (decoded.webhookUrl) {
const url = new URL(decoded.webhookUrl);
if (!urlWhitelist.includes(url.origin)) {
throw new Error('Webhook URL not in whitelist');
}
// Additional check: URL must match expected pattern
if (!decoded.webhookUrl.startsWith('https://api.yourcompany.com/webhook')) {
throw new Error('Unexpected webhook URL structure');
}
}
// Replay protection using jti
if (!decoded.jti || blacklist.has(decoded.jti)) {
throw new Error('Invalid or replayed token');
}
// Add to blacklist for replay protection
setTimeout(() => blacklist.delete(decoded.jti), 15 * 60 * 1000);
return decoded;
} catch (err) {
console.error('Webhook token validation failed:', err.message);
throw new Error('Invalid webhook token');
}
}Rate limiting is crucial for webhook endpoints. Implement per-token or per-IP rate limiting to prevent abuse even when tokens are valid:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 5, // Limit each token to 5 requests per minute
keyGenerator: (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
return token || req.ip;
},
message: 'Too many webhook requests from this token'
});
app.post('/webhook', webhookLimiter, async (req, res) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
const decoded = validateWebhookToken(token, {
expectedAudience: 'your-service-webhook',
expectedIssuer: 'your-service',
urlWhitelist: ['https://api.yourcompany.com']
});
await processWebhook(decoded.data, req.body);
res.status(200).send('OK');
} catch (err) {
res.status(401).send(err.message);
}
});For high-security scenarios, consider using short-lived JWTs combined with a nonce or challenge-response mechanism. The webhook provider includes a nonce in the initial request, and your service must respond with a signed challenge before processing the actual webhook.