sanitize-request
Version:
A TypeScript library for safe display and sanitization to prevent XSS attacks.
461 lines (378 loc) • 11.6 kB
Markdown
# Production HTML Sanitization Library
A comprehensive, production-ready HTML sanitization library for Node.js/Express applications with TypeScript support.
```bash
npm install sanitize-request
```
```bash
yarn add sanitize-request
```
```bash
pnpm add sanitize-request
```
## 🛠️ Usage Examples
### 1. Basic Express.js Integration
```typescript
import express from 'express';
import { sanitizeRequest, sanitizeStrings } from 'sanitize-request';
const app = express();
app.use(express.json());
// Apply global sanitization
app.use(sanitizeRequest());
// Or use string sanitization for simple cases
app.use(sanitizeStrings());
app.post('/api/posts', (req, res) => {
// req.body is now sanitized
console.log(req.body.content); // HTML tags filtered based on config
res.json({ success: true });
});
```
### 2. Route-Specific Configurations
```typescript
import { sanitizeRequest, getConfig, createCustomConfig } from 'sanitize-request';
// Blog posts - allow rich HTML
app.use('/api/blog', sanitizeRequest({
config: 'blog',
logWarnings: true
}));
// Comments - strict filtering
app.use('/api/comments', sanitizeRequest({
config: 'comment'
}));
// Admin panel - liberal configuration
app.use('/admin/api', sanitizeRequest({
config: 'admin',
onSanitized: (metadata) => {
console.log(`Admin content sanitized:`, metadata);
}
}));
// Custom configuration
const customConfig = createCustomConfig('base', {
allowedTags: ['p', 'br', 'strong', 'em'],
maxStringLength: 1000,
allowedAttributes: {
a: ['href']
}
});
app.use('/api/custom', sanitizeRequest({ config: customConfig }));
```
### 3. Advanced Middleware Configuration
```typescript
app.use('/api', sanitizeRequest({
config: 'strict',
skipPaths: ['/api/auth', '/api/upload'],
onSanitized: (metadata) => {
// Log to monitoring service
logger.info('Content sanitized', {
sanitized: metadata.sanitized,
warnings: metadata.warnings,
fieldsModified: metadata.fieldsModified
});
},
onError: (error, req) => {
logger.error('Sanitization failed', {
error: error.message,
path: req.path,
method: req.method
});
}
}));
```
### 4. Manual Sanitization
```typescript
import {
sanitizeString,
sanitizeRequestData,
getConfig,
SanitizationError
} from 'sanitize-request';
// Sanitize individual strings
try {
const result = sanitizeString(
'<script>alert("xss")</script><p>Hello World!</p>',
getConfig('blog')
);
console.log(result.data); // "<p>Hello World!</p>"
console.log(result.sanitized); // true
console.log(result.warnings); // ["Script tag removed"]
} catch (error) {
if (error instanceof SanitizationError) {
console.error('Sanitization failed:', error.message);
}
}
// Sanitize objects
const userData = {
name: '<b>John Doe</b>',
bio: '<script>evil()</script><p>I am a developer</p>',
email: 'john@example.com' // Won't be sanitized (not HTML)
};
const sanitized = sanitizeRequestData(userData, getConfig('blog'));
console.log(sanitized.data.name); // "<b>John Doe</b>"
console.log(sanitized.data.bio); // "<p>I am a developer</p>"
```
### 5. Configuration Profiles
```typescript
// Available configurations
const configs = {
strict: 'Minimal HTML tags only',
base: 'Basic formatting tags',
blog: 'Rich content for blog posts',
comment: 'User comments with limited formatting',
email: 'Email-safe HTML',
admin: 'Full HTML access for administrators',
liberal: 'Most HTML tags allowed'
};
// Profile comparison
const dangerousHtml = `
<script>alert('xss')</script>
<p>Hello <strong>World</strong></p>
<img src="image.jpg" alt="Test">
`;
Object.keys(configs).forEach(configName => {
const result = sanitizeString(dangerousHtml, getConfig(configName));
console.log(`${configName}:`, result.data);
});
```
### 6. Custom String Sanitization
```typescript
// Basic string cleaning
app.use(sanitizeStrings({
customSensitiveFields: ['creditCard', 'ssn'],
customSanitizer: (value) => {
return value
.replace(/[<>"'&]/g, '') // Remove dangerous characters
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
},
skipEmptyStrings: true
}));
```
### 7. Error Handling and Monitoring
```typescript
import { sanitizeRequest, SanitizationError } from 'sanitize-request';
app.use(sanitizeRequest({
config: 'blog',
onError: (error, req) => {
// Send to error tracking service
errorTracker.capture(error, {
request: {
path: req.path,
method: req.method,
body: req.body,
ip: req.ip
}
});
},
onSanitized: (metadata) => {
// Track sanitization metrics
if (metadata.warnings.length > 0) {
metrics.increment('sanitization.warnings', {
warnings: metadata.warnings.length
});
}
}
}));
// Global error handler
app.use((err, req, res, next) => {
if (err instanceof SanitizationError) {
return res.status(400).json({
error: 'Invalid content detected',
field: err.field,
message: 'Content contains unsafe HTML'
});
}
next(err);
});
```
## 🔍 Configuration Reference
### Sanitization Configs
| Config | Use Case | Allowed Tags | Max Length |
|--------|----------|--------------|------------|
| `strict` | User input forms | `b`, `i`, `em`, `strong` | 1,000 |
| `base` | General purpose | Basic formatting | 10,000 |
| `blog` | Blog posts | Rich content | 25,000 |
| `comment` | User comments | Limited formatting | 2,000 |
| `email` | Email content | Email-safe HTML | 5,000 |
| `admin` | Admin interface | Full HTML | 100,000 |
| `liberal` | Flexible content | Most HTML tags | 50,000 |
### Configuration Options
```typescript
interface SanitizationConfig {
allowedTags?: string[]; // Permitted HTML tags
allowedAttributes?: Record<string, string[]>; // Attributes per tag
stripIgnoreTag?: boolean; // Remove unknown tags
stripIgnoreTagBody?: boolean; // Remove content of unknown tags
allowEmptyTags?: boolean; // Allow tags without content
maxTagDepth?: number; // Maximum nesting depth
maxStringLength?: number; // Maximum string length
}
```
### Sensitive Fields (Auto-Protected)
These fields are automatically skipped during sanitization:
- `password`, `confirmPassword`, `passwordConfirm`
- `token`, `accessToken`, `refreshToken`, `jwt`
- `apiKey`, `secret`, `privateKey`
- `sessionId`, `csrfToken`, `authToken`
### 1. Defense in Depth
```typescript
// Multiple layers of protection
app.use(helmet()); // Security headers
app.use(rateLimit()); // Rate limiting
app.use(sanitizeRequest({ config: 'strict' })); // Input sanitization
app.use(validateInput()); // Input validation
```
### 2. Content Security Policy
```typescript
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
}));
```
### 3. Logging and Monitoring
```typescript
const sanitizationLogger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'sanitization.log' })
]
});
app.use(sanitizeRequest({
config: 'blog',
onSanitized: (metadata) => {
sanitizationLogger.info('Content sanitized', {
timestamp: new Date().toISOString(),
fieldsModified: metadata.fieldsModified,
warnings: metadata.warnings.length,
originalSize: metadata.originalSize,
finalSize: metadata.finalSize
});
}
}));
```
## 🚀 Performance Optimization
### 1. Caching Configuration
```typescript
// Cache configurations to avoid recreating them
const configCache = new Map();
const getCachedConfig = (name: string) => {
if (!configCache.has(name)) {
configCache.set(name, getConfig(name));
}
return configCache.get(name);
};
```
### 2. Conditional Sanitization
```typescript
// Only sanitize when necessary
app.use((req, res, next) => {
const contentType = req.get('Content-Type');
// Skip sanitization for file uploads
if (contentType?.includes('multipart/form-data')) {
return next();
}
// Skip for API endpoints that don't accept HTML
if (req.path.startsWith('/api/numeric-data')) {
return next();
}
sanitizeRequest({ config: 'base' })(req, res, next);
});
```
### 3. Streaming for Large Content
```typescript
import { Transform } from 'stream';
class SanitizeStream extends Transform {
constructor(private config: SanitizationConfig) {
super({ objectMode: false });
}
_transform(chunk: any, encoding: string, callback: Function) {
try {
const sanitized = sanitizeString(chunk.toString(), this.config);
callback(null, sanitized.data);
} catch (error) {
callback(error);
}
}
}
// Use for large file processing
app.post('/upload-html', (req, res) => {
req.pipe(new SanitizeStream(getConfig('liberal')))
.pipe(fs.createWriteStream('sanitized-output.html'));
});
```
## 📊 Monitoring and Analytics
### 1. Metrics Collection
```typescript
import { createPrometheusMetrics } from 'prom-client';
const sanitizationMetrics = {
totalRequests: new Counter({
name: 'sanitization_requests_total',
help: 'Total number of sanitization requests'
}),
sanitizedRequests: new Counter({
name: 'sanitization_modified_total',
help: 'Number of requests that were sanitized'
}),
warnings: new Counter({
name: 'sanitization_warnings_total',
help: 'Total number of sanitization warnings'
}),
processingTime: new Histogram({
name: 'sanitization_duration_seconds',
help: 'Time spent sanitizing requests'
})
};
app.use(sanitizeRequest({
config: 'blog',
onSanitized: (metadata) => {
sanitizationMetrics.totalRequests.inc();
if (metadata.sanitized) {
sanitizationMetrics.sanitizedRequests.inc();
}
sanitizationMetrics.warnings.inc(metadata.warnings.length);
}
}));
```
### 2. Health Checks
```typescript
app.get('/health/sanitization', (req, res) => {
try {
// Test sanitization functionality
const testResult = sanitizeString('<b>test</b>', getConfig('base'));
res.json({
status: 'healthy',
sanitizationWorking: testResult.data === '<b>test</b>',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
```
## 📝 Changelog
### v1.0.0
- Initial production release
- Complete TypeScript support
- Express middleware integration
- Multiple configuration profiles
- Comprehensive error handling
- Performance optimizations
- Security enhancements
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
## 📄 License
MIT License - see LICENSE file for details.
## 🆘 Support
For issues and questions:
- Security Issues: mindebaltru@gmail.com