Open Redirect in Nestjs with Cockroachdb
Open Redirect in Nestjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
An open redirect in a NestJS application using CockroachDB typically arises when a route accepts a user-supplied URL or path and performs an HTTP redirect without strict validation. If the application stores or references redirect targets in a CockroachDB table (for example, tenant-specific landing pages or dynamic post-login flows), an attacker may be able to manipulate the stored value or the runtime parameter to point to a malicious host.
Consider a scenario where a NestJS service retrieves a redirect URL from CockroachDB based on a user-supplied identifier (e.g., campaign or tenant ID). If the application trusts the database value and issues res.redirect(databaseUrl) without verifying that the URL is relative or belongs to an approved domain, the combination of NestJS routing and CockroachDB data becomes an open redirect vector. An attacker who can inject a malicious URL into the CockroachDB table—via SQL injection, compromised admin interfaces, or insecure backups—can cause the application to redirect users to phishing sites. Even without direct DB write access, an attacker may supply an identifier that causes the app to fetch a malicious URL from a misconfigured record.
The risk is compounded when the NestJS route also reflects user input in the redirect target (e.g., a "next" query parameter) while simultaneously using CockroachDB to validate or enrich the target. If the validation logic is incomplete (for example, only checking hostname format but not enforcing a strict allowlist), an attacker can supply a crafted URL such as https://evil.com that passes validation and triggers a redirect. Because CockroachDB is often used for multi-tenant or distributed configurations, misconfigured entries or overly permissive access controls can allow an attacker to modify redirect targets at scale, leading to widespread phishing campaigns across the application’s user base.
Insecure deserialization or ORM misuse can further blur the boundary between application logic and data layer. For example, if the NestJS app deserializes objects from CockroachDB that contain redirect URLs and uses them directly in response handlers, an attacker who can influence stored objects may be able to pivot from data manipulation to runtime behavior manipulation. This specific stack—NestJS for the application layer and CockroachDB as a distributed SQL store—does not inherently introduce open redirect, but the integration patterns (dynamic URL retrieval, per-tenant configuration, and parameterized redirects) create opportunities for abuse if validation and scoping are not rigorously enforced.
Cockroachdb-Specific Remediation in Nestjs — concrete code fixes
Remediation focuses on never trusting redirect targets, whether they come from user input, request parameters, or database rows. Use an allowlist of trusted domains, enforce relative paths where possible, and treat any external URL as untrusted. Below are concrete patterns for a NestJS service that retrieves redirect URLs from CockroachDB.
1. Validate against an allowlist and use relative redirects
Prefer relative paths for redirects. If an absolute URL is required, compare the host against an allowlist and reject anything not explicitly permitted.
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Pool } from 'pg';
@Injectable()
export class RedirectService {
private readonly allowedHosts = new Set(['app.example.com', 'cdn.example.com']);
constructor(private readonly pool: Pool) {}
async redirectTo(tenantId: string, userKey: string): Promise<{ url: string }> {
const client = await this.pool.connect();
try {
const resDb = await client.query(
'SELECT redirect_url FROM tenant_redirects WHERE tenant_id = $1 AND user_key = $2',
[tenantId, userKey],
);
if (resDb.rowCount === 0) {
throw new HttpException('Not found', HttpStatus.NOT_FOUND);
}
const candidate = resDb.rows[0].redirect_url;
// Allow only relative URLs
if (new URL(candidate, 'http://localhost').origin !== 'http://localhost') {
// Enforce allowlist for absolute URLs
try {
const parsed = new URL(candidate);
if (!this.allowedHosts.has(parsed.hostname)) {
throw new HttpException('Invalid redirect target', HttpStatus.BAD_REQUEST);
}
} catch (e) {
// If it’s not a valid absolute URL, reject it
if (!candidate.startsWith('/')) {
throw new HttpException('Invalid redirect target', HttpStatus.BAD_REQUEST);
}
}
}
return { url: candidate };
} finally {
client.release();
}
}
}
2. Use parameterized queries and strict schema constraints in CockroachDB
Ensure the CockroachDB schema enforces data types and constraints so that stored URLs are predictable. For example, store only relative paths or enforce URL format with CHECK constraints.
-- CockroachDB schema example
CREATE TABLE tenant_redirects (
tenant_id UUID NOT NULL,
user_key UUID NOT NULL,
redirect_url TEXT NOT NULL CHECK (redirect_url ~ '^(/[a-zA-Z0-9\-._~:/?#\[\]@!$&''()*+,;=]*)?$'),
PRIMARY KEY (tenant_id, user_key)
);
In NestJS, use a typed query with the pg driver to avoid injection and ensure the returned value matches the expected format.
import { Injectable } from '@nestjs/common';
import { Pool, QueryResult } from 'pg';
@Injectable()
export class TenantRedirectRepository {
constructor(private readonly pool: Pool) {}
async getRedirectUrl(tenantId: string, userKey: string): Promise {
const result: QueryResult = await this.pool.query(
`SELECT redirect_url FROM tenant_redirects WHERE tenant_id = $1 AND user_key = $2`,
[tenantId, userKey],
);
if (result.rows.length === 0) {
throw new Error('Redirect configuration not found');
}
return result.rows[0].redirect_url;
}
}
3. Reject URLs with unexpected schemes and enforce path-only redirects
When absolute URLs must be supported, normalize and validate the scheme and host. Reject javascript: or other non-HTTP(S) schemes, and avoid open redirects by defaulting to a relative path when in doubt.
function isSafeRedirect(url: string, allowedHosts: Set): boolean {
try {
const parsed = new URL(url);
return allowedHosts.has(parsed.hostname) && (parsed.protocol === 'https:' || parsed.protocol === 'http:');
} catch {
// Fallback: treat as path if not a valid absolute URL
return url.startsWith('/');
}
}
Combine this with NestJS middleware or a guard to validate the final destination before invoking res.redirect().