Open Redirect in Nestjs with Mongodb
Open Redirect in Nestjs with Mongodb — how this specific combination creates or exposes the vulnerability
An Open Redirect in a NestJS application that uses MongoDB typically arises when user-controlled input specifying a destination URL is used directly in a redirect response without strict validation. The application retrieves or receives a URL (for example, from query parameters or a request body), and if that value is passed to a redirect function such as res.redirect() or a NestJS Redirect response without verifying the target, an attacker can supply a malicious external address. This becomes a phishing aid, as users are sent to arbitrary domains that may impersonate your service.
With MongoDB as the backend, the redirect URL might be stored in or retrieved from a document (e.g., a configuration or settings collection). If an attacker can influence the stored URL—through data entry, import, or compromised administrative interfaces—and that URL is later used in a redirect without strict allowlisting or validation, the stored malicious value leads to persistent open redirect behavior across sessions. Even if the URL is provided only at runtime via query parameters, MongoDB usage does not inherently protect you; the risk is in how the application consumes and trusts external input, not in the database itself.
Common patterns that expose this vulnerability include: accepting a returnUrl query parameter and redirecting directly to it, or storing user-configured redirect targets in MongoDB and using them without domain allowlisting. Attack vectors include phishing links embedded in emails that appear to originate from your domain, or abuse of OAuth-like flows where the client-supplied redirect URI is not restricted to a set of pre-registered URIs.
Mongodb-Specific Remediation in Nestjs — concrete code fixes
To remediate open redirect when using MongoDB in NestJS, validate and constrain redirect targets on every request. Do not trust values stored in MongoDB or supplied by the client. Use a strict allowlist of permitted domains or implement a safe URL builder that only generates relative paths or known-safe origins. Below are concrete code examples demonstrating secure handling.
- Validate against an allowlist and use a relative redirect when possible:
import { Controller, Get, Query, Redirect, Req } from '@nestjs/common';
import { Request } from 'express';
const ALLOWED_HOSTS = new Set(['app.yourcompany.com', 'dashboard.yourcompany.com']);
@Controller('auth')
export class AuthController {
@Get('login')
redirectAfterLogin(@Query('returnUrl') returnUrl: string, @Req() req: Request) {
let target: string | undefined;
if (returnUrl) {
try {
const url = new URL(returnUrl, `${req.protocol}://${req.get('host')}`);
if (ALLOWED_HOSTS.has(url.hostname)) {
target = url.pathname + url.search;
}
} catch (_) {
// invalid URL — ignore
}
}
// Fallback to a safe internal route
return target ? Redirect(target) : Redirect('/dashboard');
}
}
- When the redirect target is stored in MongoDB, verify it on retrieval and enforce allowlisting before use:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { RedirectDocument } from './schemas/redirect.schema';
@Injectable()
export class RedirectService {
constructor(
@InjectModel('Redirect') private readonly redirectModel: Model,
) {}
async getSafeRedirectPath(key: string): Promise {
const entry = await this.redirectModel.findOne({ key }).exec();
if (!entry) return null;
const ALLOWED_HOSTS = new Set(['app.yourcompany.com', 'dashboard.yourcompany.com']);
try {
const url = new URL(entry.target, 'https://yourcompany.com');
if (ALLOWED_HOSTS.has(url.hostname)) {
return url.pathname + url.search;
}
} catch (_) {
// invalid or unsafe target
}
return null;
}
}
- Schema-level validation (Mongoose) to prevent storing unsafe targets:
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@Schema({ strict: true })
export class RedirectSchema {
@Prop({ required: true })
key: string;
@Prop({ required: true })
target: string;
// Validate and sanitize before saving
toObject(this: RedirectDocument) {
const obj = this.toObject ? this.toObject() : this;
try {
const url = new URL(obj.target, 'https://yourcompany.com');
const ALLOWED_HOSTS = new Set(['app.yourcompany.com', 'dashboard.yourcompany.com']);
if (!ALLOWED_HOSTS.has(url.hostname)) {
// Reject or sanitize; here we default to a safe path
obj.target = '/fallback';
}
} catch (_) {
obj.target = '/fallback';
}
return obj;
}
}
export const RedirectSchema = SchemaFactory.createForClass(RedirectSchema);
These approaches ensure that MongoDB-stored or runtime values are never used as-is in redirects. Combine this with a robust allowlist, prefer relative paths, and avoid reflecting arbitrary user input into Location headers.