Files
memind/mindspace-scan.mjs
T
john 53c5f6d9c2
Memind CI / Test, build, and release guards (pull_request) Failing after 19s
fix(wechat): scrub image_url from sessions and harden vision handoff.
Strip image_url from persisted session payloads, keep image turns scoped to the active request, and align proxy/vision/page-data paths so WeChat image history does not leak into fresh sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 10:43:24 +08:00

151 lines
4.6 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\/page-data-client\.js(?:[?#].*)?$/i,
/^\/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: [],
};
}