Files
memind/mindspace-scan.mjs
T
john 7f8d692d16 fix(agent): recover stale runs and improve new-user OA delivery
Fix DEV logout cookie clearing, materialize selected MindSpace OA assets before agent runs, and recover zombie runs from synced workspace pages. Add client run wait timeout, harness retry limits, page-edit asset forwarding, and logout/john2 scenario tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 20:05:52 +08:00

150 lines
4.5 KiB
JavaScript

const BLOCKED_ACTIVE_PATTERNS = [
{ type: 'javascript_url', pattern: /javascript:/i },
{ type: 'inline_event_handler', pattern: /\bon\w+\s*=/i },
{ type: 'embedded_active_content', pattern: /<iframe\b|<object\b|<embed\b/i },
];
const SCRIPT_TAG_PATTERN = /<script\b([^>]*)>([\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: [],
};
}