From 231265c837fb41bf41ebdbec32893cd6958df76f Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Sat, 1 Aug 2026 03:30:36 +0800 Subject: [PATCH] fix: parse dangerous constructs with AST --- verification/src/checks/security-scan.mjs | 27 ++++++++++++++++++----- verification/tests/core.test.mjs | 11 +++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/verification/src/checks/security-scan.mjs b/verification/src/checks/security-scan.mjs index 6acc0a6..b01be84 100644 --- a/verification/src/checks/security-scan.mjs +++ b/verification/src/checks/security-scan.mjs @@ -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', diff --git a/verification/tests/core.test.mjs b/verification/tests/core.test.mjs index cb00859..7ce8437 100644 --- a/verification/tests/core.test.mjs +++ b/verification/tests/core.test.mjs @@ -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 = () =>
; eval('x');"; + assert.deepEqual(findDangerousConstructs('view.tsx', runtime), [ + { file: 'view.tsx', construct: 'dangerouslySetInnerHTML' }, + { file: 'view.tsx', construct: 'eval' }, + ]); +});