Webhook Abuse in Adonisjs with Api Keys
Webhook Abuse in Adonisjs with Api Keys — how this specific combination creates or exposes the vulnerability
Webhook abuse in an AdonisJS application that relies on API keys occurs when an attacker overwhelms or manipulates the webhook endpoint(s) that are protected only by a shared secret. API keys are often passed as HTTP headers (for example, x-api-key) and are generally static, making them easy to leak or reuse if not handled carefully. If your AdonisJS routes that accept webhooks do not validate the origin of the request in addition to checking the API key, an attacker can send crafted POST requests directly to the endpoint, bypassing referral checks and triggering unintended behavior such as duplicate events, resource exhaustion, or data manipulation.
Consider a typical AdonisJS webhook handler:
// start/hooks.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class WebhooksController {
public async paymentHandler({ request, response }: HttpContextContract) {
const apiKey = request.header('x-api-key')
if (apiKey !== process.env.PAYMENT_WEBHOOK_KEY) {
return response.unauthorized({ error: 'Invalid API key' })
}
// process webhook payload
const payload = request.body()
// ... business logic
return response.ok({ received: true })
}
}
In this example, the handler checks only the API key. An attacker who discovers the key (for instance, via logs, client-side code, or a misconfigured CI/CD secret) can POST arbitrary data to the endpoint. Moreover, if the webhook performs actions such as creating records or enqueuing jobs, repeated requests can lead to inflated costs, duplicate entries, or race conditions. The risk is compounded when the API key is shared across multiple services, because a leak in one component can expose the entire integration chain. Because AdonisJS does not inherently tie API keys to IPs or enforce request signing, additional protections such as HMAC signatures or IP allowlists are necessary to prevent webhook abuse.
Another common pattern is using API keys for both outbound identification and inbound validation. Outbound requests from AdonisJS might include x-api-key to authenticate to third‑party services, but inbound webhooks that use the same key provide no proof of origin. An attacker can replay captured payloads if they can guess or obtain the key. Without additional context like a rotating signature or a verified source IP, the combination of webhooks and static API keys becomes a weak gate. This is especially dangerous for high‑volume endpoints such as payment notifications or status callbacks, where throttling and integrity checks are essential.
To detect such issues, scanners look for webhook endpoints that accept POST/PUT/DELETE without mutual TLS or HMAC verification, and flag missing origin validation even when API keys are present. They also check whether the same key is used for both outbound calls and inbound webhooks, which increases the blast radius of a leaked secret. The findings typically map to OWASP API Top 10 categories such as Broken Object Level Authorization and Excessive Data Exposure, and may intersect with standards like PCI‑DSS when payment data is involved.
Api Keys-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on adding layers of verification beyond static API keys and ensuring that webhook processing is idempotent and rate‑limited. Below are concrete, syntactically correct AdonisJS examples that you can adopt to harden your endpoints.
1. Use HMAC signatures to validate webhook origin
Instead of trusting a single API key, have the provider sign the payload with a shared secret and verify the signature in your handler. This ensures integrity and authenticity even if the API key is exposed.
// start/hooks.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import crypto from 'crypto'
export default class WebhooksController {
public async paymentHandler({ request, response }: HttpContextContract) {
const signature = request.header('x-signature')
const apiKey = request.header('x-api-key')
const rawBody = request.rawBody()
// Validate API key first
if (apiKey !== process.env.PAYMENT_WEBHOOK_KEY) {
return response.unauthorized({ error: 'Invalid API key' })
}
// Verify HMAC-SHA256 signature
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SIGNING_SECRET)
.update(rawBody)
.digest('hex')
if (!signature || signature !== `sha256=${expected}`) {
return response.badRequest({ error: 'Invalid signature' })
}
const payload = request.body()
// Ensure idempotency, e.g., check event ID
return response.ok({ received: true })
}
}
2. Separate keys for inbound and outbound usage
Avoid using the same key for outbound requests and inbound webhook validation. Store distinct keys in environment variables and reference them explicitly.
// config/env.ts
export default {
PAYMENT_WEBHOOK_KEY: Env.get('PAYMENT_WEBHOOK_KEY'),
PAYMENT_API_KEY: Env.get('PAYMENT_API_KEY'),
}
// Example outbound request using a separate key
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export async function notifyProvider(ctx: HttpContextContract) {
const client = new AxiosClient()
client.headers.set('x-api-key', env.get('PAYMENT_API_KEY'))
await client.post('https://provider.example.com/events', { data: '...' })
}
3. Add rate limiting and idempotency checks
Use AdonisJS middleware or a custom rate limiter to prevent bursts of webhook calls. Also track event IDs to avoid processing the same payload more than once.
// middleware/rate_limit.ts
import { Exception } from '@ioc:Adonis/Core/Exception'
export default class RateLimiter {
private recent = new Map()
public check(key: string) {
const now = Date.now()
const last = this.recent.get(key) || 0
if (now - last < 5_000) { // 5 seconds between identical events
throw new Exception('Too many requests', 429, 'rate_limit')
}
this.recent.set(key, now)
}
}
// usage in controller
import RateLimiter from 'App/Middleware/rate_limit'
public async paymentHandler({ request, response }: HttpContextContract) {
const idempotencyKey = request.header('idempotency-key') || request.body().eventId
new RateLimiter().check(idempotencyKey)
// ... process
}
By combining API key validation with HMAC signatures, segregated keys, and rate limiting, you reduce the likelihood of webhook abuse while keeping the integration functional and auditable.