HIGH webhook abuseadonisjsjwt tokens

Webhook Abuse in Adonisjs with Jwt Tokens

Webhook Abuse in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Webhook abuse in an AdonisJS application that uses JWT tokens can occur when webhook endpoints are treated as trusted internal calls simply because they carry a valid token. If the webhook receiver only validates the presence and signature of a JWT, an attacker who obtains a token (for example, through token leakage, insecure storage, or a compromised client) can call privileged webhook endpoints to trigger actions such as resource creation, state changes, or notifications. Because webhooks are often used for asynchronous integrations and administrative events, the impact can include unauthorized data changes, information disclosure, or amplification of other weaknesses like Insecure Direct Object References (IDOR).

JWTs in AdonisJS are commonly issued after authentication and may contain claims such as roles or scopes. However, if the application does not enforce strict context checks—such as verifying the token’s intended audience (aud), issuer (iss), and scope for webhook calls—an attacker can reuse a token issued for a less privileged context to call an administrative webhook. Additionally, replay attacks become possible when webhook payloads lack nonce or timestamp validation, allowing an intercepted request to be re-signed with a stolen token and replayed against the endpoint.

Another scenario arises when the webhook URL is exposed or predictable. If an API endpoint like /webhooks/external/resource relies solely on JWT bearer authentication without additional verification mechanisms (for example, a shared secret, HMAC, or a dedicated webhook signing key), an attacker who steals the token can impersonate legitimate services. This is especially risky when token revocation or expiration times are long, giving an attacker a wide window to misuse the token. The combination of JWT-based authentication and webhook-triggered side effects can therefore enable privilege escalation, unauthorized operations, and data manipulation if the token handling and webhook validation are not tightly coupled to the calling context.

To detect such issues, scans should examine how the application validates JWTs at webhook entry points, whether it binds tokens to specific audiences or scopes, and whether it applies replay protection. The scanner will check whether the webhook logic treats authenticated calls as inherently trusted and whether it performs minimal authorization checks before executing sensitive actions.

Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes

Remediation focuses on tightening how JWTs are validated and used for webhook calls in AdonisJS. Always enforce audience and issuer checks, apply short-lived tokens for webhook contexts, and avoid using the same token intended for user sessions for service-to-service webhook authentication. Prefer dedicated secrets or asymmetric keys for webhook signing rather than reusing access tokens.

Below are concrete code examples demonstrating secure handling.

1) Validate audience and issuer in the JWT middleware

Ensure your JWT verification explicitly checks aud and iss so tokens meant for one resource cannot be reused for another.

// start/hooks.ts
import { exceptionHandler } from '@poppinss/utils'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { createJwt } from '@ioc:Adonis/Addons/Auth'

export const jwtValidator = async ({ request, response, auth }: HttpContextContract, next: () => Promise) => {
  const token = request.header('authorization')?.replace('Bearer ', '')
  if (!token) {
    response.unauthorized({ message: 'Unauthorized: missing token' })
    return
  }

  try {
    const payload = await auth.use('api').verify(token, {
      required: true,
      aud: 'webhook-service',
      iss: 'myapp-auth-server',
    })
    // Attach verified claims to the context for downstream use
    request.authUser = payload
    await next()
  } catch (error) {
    response.unauthorized({ message: 'Invalid or mismatched token' })
  }
}

2) Use short-lived tokens for webhook calls and bind them to a scope

When issuing tokens specifically for webhook use, set a short lifetime and include a scope claim. Validate the scope on the webhook handler before performing actions.

// app/Controllers/Http/AuthController.ts
import { schema } from '@ioc:Adonis/Core/Validator'
import { DateTime } from 'luxon'
import { auth } from '@ioc:Adonis/Addons/Auth'

export async function issueWebhookToken({ request, response }) {
  const schema = schema.create({
    resource: schema.string.optional(),
  })

  const payload = await schema.validate(request.body(), {
    resource: 'billing-webhook',
  })

  const token = auth.use('api').generate({
    payload: {
      sub: 'service-billing',
      aud: 'webhook-service',
      iss: 'myapp-auth-server',
      scope: 'webhook:billing',
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(DateTime.local().plus({ minutes: 5 }).toSeconds()),
    },
    expiresIn: '5m',
  })

  return response.ok({ token })
}

3) Validate webhook signatures in addition to JWT

For higher assurance, combine JWT with a per-webhook signature (e.g., HMAC) so that even a stolen token cannot be used without the shared secret.

// app/Controllers/Http/WebhookController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import crypto from 'crypto'

export class WebhookController {
  public async handle({ request, response }: HttpContextContract) {
    const token = request.header('authorization')?.replace('Bearer ', '')
    const signature = request.header('x-webhook-signature')
    const body = request.rawBody()

    if (!token || !signature || !body) {
      return response.badRequest({ message: 'Missing authentication' })
    }

    // Verify JWT (omitted for brevity; reuse the middleware above)
    // ...

    // Verify HMAC signature using a stored secret per webhook endpoint
    const expected = crypto
      .createHmac('sha256', process.env.WEBHOOK_SIGNING_SECRET!)
      .update(body)
      .digest('hex')

    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      return response.forbidden({ message: 'Invalid webhook signature' })
    }

    // Process event
    return response.ok({ received: true })
  }
}

4) Avoid replay attacks with nonce or timestamp

Include a one-time nonce or short timestamp window when processing webhooks, and store processed nonces briefly to reject duplicates.

// app/Controllers/Http/WebhookController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { DateTime } from 'luxon'

const seenNonces = new Set() // in-memory or distributed cache in production

export class WebhookController {
  public async event({ request, response }: HttpContextContract) {
    const nonce = request.input('nonce')
    const timestamp = request.input('timestamp')

    if (!nonce || !timestamp) {
      return response.badRequest({ message: 'Missing nonce or timestamp' })
    }

    // Reject old timestamps (>2 minutes)
    const eventTime = DateTime.fromISO(timestamp)
    const now = DateTime.local()
    if (now.diff(eventTime, 'minutes').minutes! > 2) {
      return response.badRequest({ message: 'Request too old' })
    }

    if (seenNonces.has(nonce)) {
      return response.conflict({ message: 'Duplicate request' })
    }
    seenNonces.add(nonce)

    // Process event
    return response.ok({ received: true })
  }
}

These examples show how to bind JWTs to specific audiences and issuers, scope tokens to webhook actions, add HMAC signatures, and mitigate replay. Combined with runtime checks, this reduces the risk of webhook abuse when JWT tokens are involved.

Frequently Asked Questions

Can a stolen JWT be used to abuse webhooks even if the token has a short lifetime?
Yes, if the token is valid and not yet expired, it can be used to call webhook endpoints. Short lifetimes reduce the window but do not eliminate the risk; combine short-lived tokens with audience/issuer validation, scope checks, and optional HMAC signatures to limit impact.
Is relying on JWT alone sufficient to secure webhook endpoints in AdonisJS?
No. JWTs should be paired with additional controls such as audience/issuer validation, scope enforcement, replay protection (nonce/timestamp), and webhook-specific signatures. Treat webhook endpoints as high-risk surfaces and verify context beyond bearer token presence.