mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-13 07:43:43 +08:00
fix: parse dangerous constructs with AST
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parse } from '@typescript-eslint/typescript-estree';
|
||||
import { finding } from '../core/finding.mjs';
|
||||
import { relative, walk, writeJson } from '../core/files.mjs';
|
||||
import { children } from './ast-walk.mjs';
|
||||
|
||||
const textExt = new Set(['.ts', '.tsx', '.js', '.mjs', '.json', '.yml', '.yaml', '.toml', '.md']);
|
||||
const patterns = [
|
||||
@@ -11,6 +13,25 @@ const patterns = [
|
||||
['generic-secret-assignment', /(?:secret|token|password|api[_-]?key)\s*[:=]\s*['"][^'"\n]{12,}['"]/i],
|
||||
];
|
||||
|
||||
export function findDangerousConstructs(file, text) {
|
||||
const ast = parse(text, {
|
||||
jsx: file.endsWith('.tsx') || file.endsWith('.jsx'),
|
||||
errorOnUnknownASTType: false,
|
||||
});
|
||||
const constructs = new Set();
|
||||
const visit = (node) => {
|
||||
if (node.type === 'JSXAttribute' && node.name?.name === 'dangerouslySetInnerHTML') {
|
||||
constructs.add('dangerouslySetInnerHTML');
|
||||
}
|
||||
if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'eval') {
|
||||
constructs.add('eval');
|
||||
}
|
||||
for (const child of children(node)) visit(child);
|
||||
};
|
||||
visit(ast);
|
||||
return [...constructs].map((construct) => ({ file, construct }));
|
||||
}
|
||||
|
||||
export async function checkSecurityScan(ctx) {
|
||||
const files = walk(ctx.config.root, (file) => textExt.has(path.extname(file)) && !file.includes('/verification/artifacts/'));
|
||||
const hits = [];
|
||||
@@ -30,11 +51,7 @@ export async function checkSecurityScan(ctx) {
|
||||
evidence: [target], remediation: 'Remove and rotate confirmed credentials; replace false positives with safe fixtures.',
|
||||
});
|
||||
const dangerous = files.filter((file) => ['.ts', '.tsx', '.js', '.mjs'].includes(path.extname(file))).flatMap((file) => {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
return [
|
||||
...(text.includes('dangerouslySetInnerHTML') ? [{ file: relative(ctx.config.root, file), construct: 'dangerouslySetInnerHTML' }] : []),
|
||||
...(text.match(/\beval\s*\(/) ? [{ file: relative(ctx.config.root, file), construct: 'eval' }] : []),
|
||||
];
|
||||
return findDangerousConstructs(relative(ctx.config.root, file), fs.readFileSync(file, 'utf8'));
|
||||
});
|
||||
finding(ctx, {
|
||||
id: 'security.dangerous-constructs', category: 'security', title: 'Dangerous runtime constructs are inventoried',
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from 'node:test';
|
||||
import { numericCandidate, prepareActionState } from '../src/browser/action-state.mjs';
|
||||
import { getConfig } from '../src/config.mjs';
|
||||
import { latestDeployment } from '../src/checks/deployment.mjs';
|
||||
import { findDangerousConstructs } from '../src/checks/security-scan.mjs';
|
||||
import { redact, redactText } from '../src/core/redact.mjs';
|
||||
import { escapeXml } from '../src/core/xml.mjs';
|
||||
|
||||
@@ -51,3 +52,13 @@ test('selects the newest Cloudflare production deployment', () => {
|
||||
assert.equal(latestDeployment(output)?.Source, 'abcdef1');
|
||||
assert.equal(latestDeployment('not json'), null);
|
||||
});
|
||||
|
||||
test('dangerous construct scan ignores matcher text and finds runtime use', () => {
|
||||
const scanner = "const name = 'dangerouslySetInnerHTML'; const matcher = /eval\\s*\\(/;";
|
||||
assert.deepEqual(findDangerousConstructs('scanner.mjs', scanner), []);
|
||||
const runtime = "export const View = () => <div dangerouslySetInnerHTML={{ __html: 'x' }} />; eval('x');";
|
||||
assert.deepEqual(findDangerousConstructs('view.tsx', runtime), [
|
||||
{ file: 'view.tsx', construct: 'dangerouslySetInnerHTML' },
|
||||
{ file: 'view.tsx', construct: 'eval' },
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user