const express = require('express');
const { URL } = require('url');
const app = express();
// Utility functions for header security
function sanitizeHeaderValue(input) {
if (typeof input !== 'string') {
throw new Error('Header value must be a string');
}
// Remove CRLF and control characters
const sanitized = input
.replace(/[\r\n]/g, '') // Remove CRLF
.replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control chars
.trim();
if (sanitized.length > 1000) {
throw new Error('Header value too long');
}
return sanitized;
}
function validateUrl(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTP/HTTPS
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
// Optional: Whitelist allowed domains
const allowedDomains = ['example.com', 'trusted-site.com'];
if (!allowedDomains.includes(url.hostname)) {
return false;
}
return true;
} catch (error) {
return false;
}
}
function validateTrackingId(trackingId) {
// Only allow alphanumeric characters and hyphens
return /^[a-zA-Z0-9-]+$/.test(trackingId) && trackingId.length <= 50;
}
// Security headers middleware
function addSecurityHeaders(req, res, next) {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'self'");
next();
}
app.use(addSecurityHeaders);
app.use(express.json({ limit: '1mb' }));
// SECURE: Validated redirect
app.get('/redirect', (req, res) => {
try {
const url = req.query.url;
if (!url || typeof url !== 'string') {
return res.status(400).send('URL parameter required');
}
const sanitizedUrl = sanitizeHeaderValue(url);
if (!validateUrl(sanitizedUrl)) {
return res.status(400).send('Invalid or untrusted URL');
}
res.redirect(sanitizedUrl);
} catch (error) {
console.error('Redirect error:', error.message);
res.status(400).send('Invalid redirect request');
}
});
// SECURE: Validated custom headers
app.get('/api/track', (req, res) => {
try {
const trackingId = req.query.tracking_id;
const source = req.query.source;
// Validate tracking ID
if (!trackingId || !validateTrackingId(trackingId)) {
return res.status(400).json({ error: 'Invalid tracking ID' });
}
// Validate and sanitize source
if (!source || typeof source !== 'string') {
return res.status(400).json({ error: 'Source parameter required' });
}
const sanitizedSource = sanitizeHeaderValue(source);
// Whitelist allowed sources
const allowedSources = ['web', 'mobile', 'api', 'email'];
if (!allowedSources.includes(sanitizedSource)) {
return res.status(400).json({ error: 'Invalid source' });
}
// Safe header setting
res.setHeader('X-Tracking-ID', trackingId);
res.setHeader('X-Source', sanitizedSource);
res.json({
message: 'Tracking recorded',
tracking_id: trackingId,
source: sanitizedSource
});
} catch (error) {
console.error('Tracking error:', error.message);
res.status(400).json({ error: 'Invalid tracking request' });
}
});
// SECURE: Validated cookie setting
app.post('/set-theme', (req, res) => {
try {
const { theme, preferences } = req.body;
// Validate theme
const allowedThemes = ['light', 'dark', 'auto'];
if (!theme || !allowedThemes.includes(theme)) {
return res.status(400).send('Invalid theme');
}
// Validate preferences (JSON string)
let validatedPreferences = '{}';
if (preferences) {
try {
const parsed = JSON.parse(preferences);
// Validate preference structure
if (typeof parsed === 'object' && parsed !== null) {
validatedPreferences = JSON.stringify(parsed);
}
} catch (e) {
return res.status(400).send('Invalid preferences format');
}
}
// Safe cookie setting with security options
res.cookie('theme', theme, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});
res.cookie('preferences', validatedPreferences, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000
});
res.json({
message: 'Theme updated successfully',
theme: theme
});
} catch (error) {
console.error('Theme update error:', error.message);
res.status(400).send('Invalid theme update request');
}
});
// Error handling middleware
app.use((error, req, res, next) => {
console.error('Application error:', error);
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;