Open Redirect in Nestjs with Hmac Signatures
Open Redirect in Nestjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
An Open Redirect occurs when an application redirects a user to an arbitrary URL without proper validation. In NestJS, this often arises in authentication or consent flows where a redirect URL is supplied by the client. Combining this pattern with Hmac Signatures introduces a subtle but serious risk: if the signature is computed over only a subset of the request parameters (or computed before validation), an attacker can preserve a valid signature while changing the redirect target to a malicious site.
Consider a typical OAuth-like flow where a client sends redirect_uri and a set of parameters, and the server signs these parameters using an Hmac key. If the server signs the parameters before confirming that redirect_uri is on an allowlist, the signature remains valid when the client echoes the parameters back after the user authorization step. The server then trusts the signed payload and redirects to the attacker-controlled URL because the signature matches, even though the redirect location is untrusted.
This becomes a practical vulnerability when the Hmac is implemented without canonicalization or without including a strict redirect_uri in the signed scope. For example, an endpoint that accepts url as a query parameter and computes the signature over other parameters but not url enables an attacker to supply any external destination while the signature still validates. The server-side code may look correct—verifying the Hmac and then performing a redirect—but the trust boundary is misaligned: the signature should cover all user-influenced data that affects security decisions, especially the redirect target.
In NestJS, this can manifest in controller actions that parse query or body parameters, generate an Hmac, and then proceed with a redirect based on user input. If the developer does not validate the redirect destination against a strict whitelist of allowed origins, and if the Hmac does not bind to that destination, the application becomes susceptible to phishing and session manipulation attacks. The vulnerability is not in Hmac itself but in how the signed data is constructed and validated in the context of redirects.
Hmac Signatures-Specific Remediation in Nestjs — concrete code fixes
To remediate Open Redirect when using Hmac Signatures in NestJS, you must ensure that the redirect target is validated before signature computation and that the signature covers the exact value used in the redirect. Below are concrete, secure patterns with working code examples.
1. Validate the redirect URI against an allowlist before signing
Never sign a redirect URI that is not explicitly allowed. Define a set of trusted origins and perform a strict equality check (avoid prefix or substring matches). Only after validation should you include the URI in the data that is signed.
import { Injectable } from '@nestjs/common';
import { createHmac } from 'crypto';
@Injectable()
export class AuthService {
private readonly ALLOWED_REDIRECT_URIS = new Set([
'https://app.example.com/callback',
'https://app.example.com/complete',
]);
private readonly HMAC_KEY = process.env.HMAC_KEY; // 256-bit secret stored securely
validateRedirectUri(uri: string): boolean {
return this.ALLOWED_REDIRECT_URIS.has(uri);
}
signPayload(data: Record<string, string>): string {
const sortedKeys = Object.keys(data).sort();
const canonical = sortedKeys.map((k) => `${k}=${data[k]}`).join('&');
return createHmac('sha256', this.HMAC_KEY).update(canonical).digest('hex');
}
buildSignedAuthUrl(baseUrl: string, redirectUri: string, state: string): { url: string, signature: string } {
if (!this.validateRedirectUri(redirectUri)) {
throw new Error('Invalid redirect URI');
}
const payload = {
redirect_uri: redirectUri,
state,
ts: Date.now().toString(),
};
const signature = this.signPayload(payload);
const url = new URL(baseUrl);
url.searchParams.set('redirect_uri', redirectUri);
url.searchParams.set('state', state);
url.searchParams.set('ts', payload.ts);
url.searchParams.set('signature', signature);
return { url: url.toString(), signature };
}
}
2. Include the exact redirect URI in the Hmac scope and verify on callback
When the client returns the callback request, recompute the Hmac over the same canonical string, including the received redirect_uri, and ensure it matches the provided signature before performing any redirect. This binds the signature to the actual destination used.
import { Controller, Request, Query } from '@nestjs/common';
import { createHmac } from 'crypto';
@Controller('auth/callback')
export class AuthCallbackController {
private readonly HMAC_KEY = process.env.HMAC_KEY;
private readonly ALLOWED_REDIRECT_URIS = new Set([
'https://app.example.com/callback',
'https://app.example.com/complete',
]);
verifyHmac(params: Record<string, string>): boolean {
const { signature, ...data } = params;
const sortedKeys = Object.keys(data).sort();
const canonical = sortedKeys.map((k) => `${k}=${data[k]}`).join('&');
const expected = createHmac('sha256', this.HMAC_KEY).update(canonical).digest('hex');
return timingSafeEqual(signature, expected);
}
timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
@Get()
handleCallback(@Query() query: Record<string, string>) {
if (!this.verifyHmac(query)) {
throw new Error('Invalid signature');
}
const { redirect_uri, state } = query;
if (!this.ALLOWED_REDIRECT_URIS.has(redirect_uri)) {
throw new Error('Redirect URI not allowed');
}
// Safe to redirect to the validated URI
return { redirectTo: redirect_uri, state, message: 'Authentication successful' };
}
}
3. Canonicalization and parameter encoding
Ensure consistent encoding by normalizing parameter names and values before signing. Avoid including extra parameters that do not affect the redirect decision, and always sort keys to produce a deterministic string. This prevents subtle mismatches between the signed payload and the verification payload due to ordering or encoding differences.
These patterns ensure that the redirect target is validated and bound into the Hmac, eliminating the Open Redirect risk while preserving the utility of signed URLs in your NestJS application.