FRE-4520: Fix security vulnerabilities in notification template system
- Fix HTML injection vulnerability with proper entity encoding - Fix rate limit cleanup bug (count vs timestamp confusion) - Add URL validation to prevent open redirect attacks - Add expiration to in-memory deduplication entries - Use Zod schema for config validation - Add email format validation All 29 tests passing. Ready for Code Reviewer final review. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -10,6 +10,7 @@ import { AllDefaultTemplates, DEFAULT_LOCALE } from '../templates/default-templa
|
||||
|
||||
const CACHE_TTL_MS = 300000;
|
||||
const VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
|
||||
const TRUSTED_DOMAINS = ['shieldai.com', 'app.shieldai.com', 'api.shieldai.com'];
|
||||
|
||||
export class TemplateService {
|
||||
private static instance: TemplateService;
|
||||
@@ -165,19 +166,54 @@ export class TemplateService {
|
||||
varMap.set(v.name, v);
|
||||
}
|
||||
|
||||
return text.replace(VARIABLE_PATTERN, (match, varName) => {
|
||||
const isHtmlContext = text.includes('<') && text.includes('>');
|
||||
|
||||
return text.replace(VARIABLE_PATTERN, (match, varName) => {
|
||||
const value = variables[varName];
|
||||
if (value !== undefined) {
|
||||
return String(value);
|
||||
const stringValue = String(value);
|
||||
if (this.isUrlVariable(varName)) {
|
||||
return this.validateUrl(stringValue);
|
||||
}
|
||||
return isHtmlContext ? this.escapeHtml(stringValue) : stringValue;
|
||||
}
|
||||
const schemaVar = varMap.get(varName);
|
||||
if (schemaVar?.defaultValue !== undefined) {
|
||||
return schemaVar.defaultValue;
|
||||
const defaultValue = String(schemaVar.defaultValue);
|
||||
if (this.isUrlVariable(varName)) {
|
||||
return this.validateUrl(defaultValue);
|
||||
}
|
||||
return isHtmlContext ? this.escapeHtml(defaultValue) : defaultValue;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
private escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
private validateUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (TRUSTED_DOMAINS.includes(parsed.hostname)) {
|
||||
return url;
|
||||
}
|
||||
return '/';
|
||||
} catch {
|
||||
return '/';
|
||||
}
|
||||
}
|
||||
|
||||
private isUrlVariable(varName: string): boolean {
|
||||
return varName.endsWith('_url');
|
||||
}
|
||||
|
||||
private getCached(templateId: string, locale: string): TemplateDefinition | null {
|
||||
const cacheKey = this.getCacheKey(templateId, locale);
|
||||
const entry = this.cache.get(cacheKey);
|
||||
|
||||
Reference in New Issue
Block a user