const BLOCKED_ACTIVE_PATTERNS = [ { type: 'javascript_url', pattern: /javascript:/i }, { type: 'inline_event_handler', pattern: /\bon\w+\s*=/i }, { type: 'embedded_active_content', pattern: /]*)>([\s\S]*?)<\/script>/gi; const SCRIPT_SRC_PATTERN = /\bsrc\s*=\s*(['"])([^'"]+)\1/i; const TRUSTED_SCRIPT_SRC_PATTERNS = [ /^\/assets\/chart\.umd\.min\.js(?:[?#].*)?$/i, /^https:\/\/cdn\.jsdelivr\.net\/npm\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js(?:[?#].*)?$/i, /^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/Chart\.js\/[^/]+\/chart\.(?:umd\.)?min\.js(?:[?#].*)?$/i, /^https:\/\/unpkg\.com\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js(?:[?#].*)?$/i, ]; const MAX_ZIP_COMPRESSION_RATIO = 100; const MAX_ZIP_LOCAL_HEADERS = 512; function isZipContainer(mimeType) { return ( mimeType.includes('openxmlformats') || mimeType.includes('msword') || mimeType.includes('ms-excel') || mimeType.includes('ms-powerpoint') ); } function isTrustedScriptSrc(src) { const normalized = String(src ?? '').trim().replace(/^\/\//, 'https://'); return TRUSTED_SCRIPT_SRC_PATTERNS.some((pattern) => pattern.test(normalized)); } function scanScriptTags(sample, { htmlActiveContentPolicy }) { const scripts = [...sample.matchAll(SCRIPT_TAG_PATTERN)]; if (!scripts.length) return null; if (htmlActiveContentPolicy !== 'sandbox_warn') { return { scanStatus: 'blocked', riskLevel: 'high', findings: ['text_active_content'], }; } const untrusted = scripts.some((match) => { const attrs = match[1] ?? ''; const src = attrs.match(SCRIPT_SRC_PATTERN)?.[2] ?? null; return src ? !isTrustedScriptSrc(src) : false; }); if (untrusted) { return { scanStatus: 'blocked', riskLevel: 'high', findings: ['text_active_content'], }; } return { scanStatus: 'warned', riskLevel: 'medium', findings: ['trusted_html_active_content'], }; } function scanTextContent(buffer, options = {}) { const sample = buffer.subarray(0, Math.min(buffer.length, 256 * 1024)).toString('utf8'); for (const { type, pattern } of BLOCKED_ACTIVE_PATTERNS) { if (pattern.test(sample)) { return { scanStatus: 'blocked', riskLevel: 'high', findings: [type], }; } } const scriptFinding = scanScriptTags(sample, options); if (scriptFinding) return scriptFinding; return null; } function scanZipStructure(buffer) { if (buffer.subarray(0, 2).toString('ascii') !== 'PK') { return { scanStatus: 'blocked', riskLevel: 'medium', findings: ['zip_signature_mismatch'], }; } let offset = 0; let headers = 0; while (offset + 30 <= buffer.length && headers < MAX_ZIP_LOCAL_HEADERS) { if (buffer.subarray(offset, offset + 2).toString('ascii') !== 'PK') break; const compressionMethod = buffer.readUInt16LE(offset + 8); const compressedSize = buffer.readUInt32LE(offset + 18); const uncompressedSize = buffer.readUInt32LE(offset + 22); const nameLength = buffer.readUInt16LE(offset + 26); const extraLength = buffer.readUInt16LE(offset + 28); const headerSize = 30 + nameLength + extraLength; if (headerSize <= 0 || offset + headerSize > buffer.length) break; if (compressionMethod !== 0 && compressedSize > 0 && uncompressedSize / compressedSize > MAX_ZIP_COMPRESSION_RATIO) { return { scanStatus: 'blocked', riskLevel: 'critical', findings: ['zip_bomb_ratio'], }; } headers += 1; offset += headerSize; if (compressedSize > 0) { offset += compressedSize; } } return null; } export function runBasicFileScan(buffer, { filename, mimeType, htmlActiveContentPolicy = 'block' }) { if (!Buffer.isBuffer(buffer) || buffer.length === 0) { return { scanStatus: 'blocked', riskLevel: 'high', findings: ['empty_file'], }; } if (mimeType.startsWith('text/') || mimeType === 'application/pdf') { const textFinding = scanTextContent(buffer, { filename, mimeType, htmlActiveContentPolicy }); if (textFinding) return textFinding; } if (mimeType === 'application/pdf' && buffer.subarray(0, 5).toString('ascii') !== '%PDF-') { return { scanStatus: 'blocked', riskLevel: 'medium', findings: ['pdf_signature_mismatch'], }; } if (isZipContainer(mimeType)) { const zipFinding = scanZipStructure(buffer); if (zipFinding) return zipFinding; } return { scanStatus: 'passed', riskLevel: 'none', findings: [], }; }