Crlf Injection in Strapi (Typescript)
Crlf Injection in Strapi with Typescript — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject a CRLF sequence (\r\n) into a header or query parameter, causing the application to prematurely terminate a header line and inject additional headers or split responses. Strapi, as a Node.js-based headless CMS, often builds dynamic redirect URLs, response headers, or HTTP status messages using user-controlled input. When these values are concatenated into strings that are later passed to Node.js HTTP utilities without sanitization, the CRLF characters can break the protocol structure.
In a Typescript codebase, this typically manifests in custom controllers or services where input such as a redirect or next parameter is used directly. For example, constructing a redirect URL by string interpolation like `/login?next=${redirectUrl}` and then passing the result to ctx.redirect() can become dangerous if redirectUrl contains %0D%0A or raw \r\n. The CRLF characters are interpreted by the underlying HTTP layer, enabling response splitting, header injection, or cache poisoning. Strapi’s middleware chain in Typescript does not inherently sanitize these values, so the developer must explicitly validate and encode them.
Another vector involves logging or error messages where user input is reflected. If a Typescript service includes user data in log lines or error responses without sanitization, injected CRLF can forge log entries or facilitate HTTP response splitting when logs are later rendered or monitored. Because Strapi exposes HTTP endpoints and often integrates with webhook or callback URLs defined by content editors, the attack surface includes both developer-defined routes and content-configurable URLs. The combination of dynamic URL generation, unvalidated input, and Node.js’s permissive header handling makes Strapi+Typescript implementations susceptible to CRLF Injection when input is treated as trusted.
Typescript-Specific Remediation in Strapi — concrete code fixes
Remediation focuses on input validation, strict allowlisting, and safe encoding. Never trust redirect URLs or header-related parameters. Use a strict allowlist for redirect targets or derive internal identifiers only. Encode user input before it reaches headers or URL construction. Below are concrete Typescript examples for Strapi controllers.
1) Safe redirect with allowlist and validation:
import { z } from 'zod';
const redirectSchema = z.object({
next: z.string().regex(/^\/(?:dashboard|profile|settings)$/).optional(),
});
export default async (ctx: Context) => {
const safe = redirectSchema.safeParse(ctx.query);
const next = safe.success & safe.data.next ? safe.data.next : '/home';
ctx.redirect(next);
};
This ensures next is limited to known safe paths, eliminating CRLF and open redirect risks.
2) Encode user input before header use:
import sanitizeHeader from 'sanitize-header';
export const buildLocationHeader = (userValue: string): string => {
const clean = sanitizeHeader(userValue, { lowercase: false });
return `/callback?token=${clean}`;
};
export default async (ctx: Context) => {
const location = buildLocationHeader(ctx.request.query.token as string);
ctx.set('X-Redirect-To', location);
ctx.status = 302;
ctx.body = '';
};
sanitize-header removes or encodes characters that could introduce CRLF, ensuring the header value remains a single line.
3) Avoid concatenating user input into status or header lines:
interface ErrorResponse {
message: string;
path?: string;
}
export const safeError = (ctx: Context, message: string, path?: string): void => {
const payload: ErrorResponse = {
message: message.replace(/[\r\n]+/g, ' ').substring(0, 200),
...(path && { path: path.replace(/[\r\n]+/g, '') }),
};
ctx.status = 400;
ctx.body = payload;
};
Stripping CR and LF characters from user-supplied fields before inclusion in JSON responses prevents response splitting when logs or downstream systems consume these outputs.
4) Validate URL parameters used in Strapi services:
const isSafeUrl = (url: string): boolean => {
try {
const u = new URL(url, 'http://localhost');
return u.hostname === 'app.example.com' && u.protocol === 'https:';
} catch {
return false;
}
};
export const redirectToExternal = (url: string) => {
if (!isSafeUrl(url)) throw new Error('Invalid redirect target');
return url;
};
This prevents CRLF and open redirect by ensuring the URL is well-formed and restricted to an expected host.