its-compiler-js
Version:
JavaScript/TypeScript implementation of the Instruction Template Specification (ITS) compiler
332 lines • 12.6 kB
JavaScript
/**
* Security validation and protection for ITS Compiler
*/
import { URL } from 'url';
import { ITSSecurityError } from './types.js';
export class SecurityValidator {
constructor(config) {
this.config = config;
}
/**
* Validate template content for security issues
*/
validateTemplate(template) {
// Check template size
const templateStr = JSON.stringify(template);
if (templateStr.length > this.config.maxTemplateSize) {
throw new ITSSecurityError(`Template too large: ${templateStr.length} bytes`, 'template_size', 'SIZE_LIMIT_EXCEEDED');
}
// Check content elements count
if (template.content && Array.isArray(template.content)) {
if (template.content.length > this.config.maxContentElements) {
throw new ITSSecurityError(`Too many content elements: ${template.content.length}`, 'content_elements', 'SIZE_LIMIT_EXCEEDED');
}
// Validate content structure
this.validateContent(template.content, 0);
}
// Validate variables - THIS WAS THE BUG!
// The original code only validated variables if template.variables existed,
// but we need to validate ALL variables including dangerous keys
if (template.variables) {
this.validateVariables(template.variables, '', 0);
}
// Validate extensions
if (template.extends && Array.isArray(template.extends)) {
for (const url of template.extends) {
this.validateSchemaUrl(url);
}
}
}
/**
* Validate content elements recursively
*/
validateContent(content, depth) {
if (depth > this.config.maxNestingDepth) {
throw new ITSSecurityError(`Content nesting too deep: ${depth}`, 'nesting_depth', 'SIZE_LIMIT_EXCEEDED');
}
for (const element of content) {
if (element.type === 'text') {
this.validateTextContent(element.text);
}
else if (element.type === 'conditional') {
this.validateConditionalExpression(element.condition);
if (element.content) {
this.validateContent(element.content, depth + 1);
}
if (element.else) {
this.validateContent(element.else, depth + 1);
}
}
else if (element.type === 'placeholder') {
this.validatePlaceholderConfig(element.config);
}
}
}
/**
* Validate text content for malicious patterns
*/
validateTextContent(text) {
const dangerousPatterns = [
/<script[^>]*>.*?<\/script>/gi,
/javascript\s*:/gi,
/data\s*:\s*text\/html/gi,
/eval\s*\(/gi,
/Function\s*\(/gi,
/setTimeout\s*\(/gi,
/setInterval\s*\(/gi,
/document\.\w+/gi,
/window\.\w+/gi,
/\\x[0-9a-fA-F]{2}/gi,
/\\u[0-9a-fA-F]{4}/gi,
/%[0-9a-fA-F]{2}/gi,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(text)) {
throw new ITSSecurityError('Malicious content detected in text', 'content_validation', 'MALICIOUS_CONTENT', text.substring(0, 100));
}
}
}
/**
* Validate conditional expressions
*/
validateConditionalExpression(expression) {
if (expression.length > this.config.maxExpressionLength) {
throw new ITSSecurityError(`Expression too long: ${expression.length} characters`, 'expression_length', 'SIZE_LIMIT_EXCEEDED');
}
const dangerousPatterns = [
/__\w+__/,
/exec\s*\(/,
/eval\s*\(/,
/import\s+/,
/open\s*\(/,
/subprocess/,
/os\./,
/sys\./,
/globals\s*\(/,
/locals\s*\(/,
/getattr\s*\(/,
/setattr\s*\(/,
/hasattr\s*\(/,
/delattr\s*\(/,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(expression)) {
throw new ITSSecurityError('Dangerous pattern in conditional expression', 'expression_validation', 'MALICIOUS_CONTENT', expression.substring(0, 100));
}
}
}
/**
* Validate placeholder configuration
*/
validatePlaceholderConfig(config) {
for (const [_key, value] of Object.entries(config)) {
if (typeof value === 'string') {
this.validateTextContent(value);
}
else if (typeof value === 'object' && value !== null) {
this.validatePlaceholderConfig(value);
}
}
}
/**
* Validate variables object
*/
validateVariables(variables, path, depth) {
if (depth > this.config.maxNestingDepth) {
throw new ITSSecurityError(`Variable nesting too deep at ${path}`, 'variable_nesting', 'SIZE_LIMIT_EXCEEDED');
}
// Check for prototype pollution by examining the object's prototype
if (variables.__proto__ && variables.__proto__ !== Object.prototype) {
throw new ITSSecurityError('Prototype pollution detected: __proto__ has been modified', 'prototype_pollution', 'MALICIOUS_CONTENT');
}
// Check for dangerous properties that might not show up in Object.entries()
// This catches cases where __proto__ was used as an object literal key
const dangerousProps = ['__proto__', 'constructor', 'prototype'];
for (const prop of dangerousProps) {
if (Object.prototype.hasOwnProperty.call(variables, prop)) {
throw new ITSSecurityError(`Dangerous property detected: ${prop}`, 'dangerous_property', 'MALICIOUS_CONTENT');
}
}
// Use Object.getOwnPropertyNames to catch all properties, including non-enumerable ones
const allKeys = Object.getOwnPropertyNames(variables);
for (const key of allKeys) {
// Check for dangerous variable names
if (this.isDangerousVariableName(key)) {
throw new ITSSecurityError(`Dangerous variable name: ${key}`, 'variable_name', 'MALICIOUS_CONTENT');
}
}
// Now validate the values using Object.entries (for enumerable properties)
for (const [key, value] of Object.entries(variables)) {
const currentPath = path ? `${path}.${key}` : key;
if (typeof value === 'string') {
this.validateTextContent(value);
}
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
this.validateVariables(value, currentPath, depth + 1);
}
else if (Array.isArray(value)) {
if (value.length > 1000) {
throw new ITSSecurityError(`Array too large at ${currentPath}: ${value.length} items`, 'array_size', 'SIZE_LIMIT_EXCEEDED');
}
for (let i = 0; i < value.length; i++) {
if (typeof value[i] === 'string') {
this.validateTextContent(value[i]);
}
else if (typeof value[i] === 'object' && value[i] !== null) {
this.validateVariables({ [i]: value[i] }, `${currentPath}[${i}]`, depth + 1);
}
}
}
}
}
/**
* Check if variable name is dangerous
*/
isDangerousVariableName(name) {
const dangerousNames = new Set([
'__proto__',
'constructor',
'prototype',
'__builtins__',
'__globals__',
'__locals__',
'__import__',
'exec',
'eval',
'compile',
'open',
'input',
'globals',
'locals',
'vars',
'dir',
'getattr',
'setattr',
'hasattr',
'delattr',
'function',
'this',
'window',
'document',
'global',
'process',
]);
return dangerousNames.has(name.toLowerCase()) || name.startsWith('__');
}
/**
* Validate schema URL for SSRF protection
*/
validateSchemaUrl(url) {
try {
const parsedUrl = new URL(url);
// Check protocol
if (!parsedUrl.protocol.startsWith('https:')) {
if (!this.config.allowHttp || parsedUrl.protocol !== 'http:') {
throw new ITSSecurityError(`Protocol not allowed: ${parsedUrl.protocol}`, 'url_protocol', 'SSRF_BLOCKED');
}
}
// Block dangerous protocols
const dangerousProtocols = ['file:', 'ftp:', 'gopher:', 'ldap:', 'dict:', 'data:'];
if (dangerousProtocols.includes(parsedUrl.protocol)) {
throw new ITSSecurityError(`Dangerous protocol blocked: ${parsedUrl.protocol}`, 'url_protocol', 'SSRF_BLOCKED');
}
// Check hostname
if (parsedUrl.hostname) {
this.validateHostname(parsedUrl.hostname);
}
// Check for domain allowlist
if (this.config.domainAllowlist && this.config.domainAllowlist.length > 0) {
if (!this.isDomainAllowed(parsedUrl.hostname)) {
throw new ITSSecurityError(`Domain not in allowlist: ${parsedUrl.hostname}`, 'domain_allowlist', 'SSRF_BLOCKED');
}
}
// Check for path traversal
if (parsedUrl.pathname.includes('..')) {
throw new ITSSecurityError('Path traversal detected in URL', 'path_traversal', 'SSRF_BLOCKED');
}
}
catch (error) {
if (error instanceof ITSSecurityError) {
throw error;
}
throw new ITSSecurityError(`Invalid URL: ${url}`, 'url_validation', 'SSRF_BLOCKED');
}
}
/**
* Validate hostname for SSRF protection
*/
validateHostname(hostname) {
// Block localhost variants
if (this.config.blockLocalhost) {
const localhostVariants = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
if (localhostVariants.includes(hostname.toLowerCase())) {
throw new ITSSecurityError(`Localhost access blocked: ${hostname}`, 'localhost_blocked', 'SSRF_BLOCKED');
}
}
// Block private networks
if (this.config.blockPrivateNetworks) {
if (this.isPrivateNetwork(hostname)) {
throw new ITSSecurityError(`Private network access blocked: ${hostname}`, 'private_network_blocked', 'SSRF_BLOCKED');
}
}
}
/**
* Check if hostname is in private network range
*/
isPrivateNetwork(hostname) {
// Basic check for common private IP ranges
const privateRanges = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./, // Link-local
];
return privateRanges.some(range => range.test(hostname));
}
/**
* Check if domain is allowed
*/
isDomainAllowed(hostname) {
if (!this.config.domainAllowlist) {
return true;
}
return this.config.domainAllowlist.some(allowedDomain => {
// Exact match
if (hostname === allowedDomain) {
return true;
}
// Subdomain match
if (hostname.endsWith('.' + allowedDomain)) {
return true;
}
return false;
});
}
}
/**
* Default security configuration
*/
export const DEFAULT_SECURITY_CONFIG = {
allowHttp: false,
blockLocalhost: true,
blockPrivateNetworks: true,
maxTemplateSize: 1024 * 1024, // 1MB
maxContentElements: 1000,
maxNestingDepth: 10,
maxExpressionLength: 500,
requestTimeout: 10000, // 10 seconds
};
/**
* Development security configuration (more permissive)
*/
export const DEVELOPMENT_SECURITY_CONFIG = {
allowHttp: true,
blockLocalhost: false,
blockPrivateNetworks: false,
maxTemplateSize: 5 * 1024 * 1024, // 5MB
maxContentElements: 2000,
maxNestingDepth: 15,
maxExpressionLength: 1000,
requestTimeout: 30000, // 30 seconds
};
//# sourceMappingURL=security.js.map