fix(GOV-009): 封堵外部脚本边界绕过

This commit is contained in:
2026-08-13 19:49:42 +08:00
parent cb550e3d5c
commit 95b8611bd9
5 changed files with 31 additions and 18 deletions
@@ -2,6 +2,7 @@ import { readdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import ts from 'typescript';
import { parse } from '@vue/compiler-sfc';
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.vue']);
const INTERNAL_MESSAGE_MARKERS = ['MessageEnvelope', 'message-contracts'];
@@ -20,16 +21,18 @@ async function collectSourceFiles(directory) {
function extractTypeScriptSource(sourcePath, sourceText) {
if (path.extname(sourcePath) !== '.vue') return sourceText;
const externalScript = sourceText.match(/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/i);
const { descriptor, errors } = parse(sourceText, { filename: sourcePath });
const externalScript = descriptor.script?.src ?? descriptor.scriptSetup?.src;
if (externalScript) {
throw new Error(
`Web Vue 组件禁止使用外部 script src:${path.basename(sourcePath)} -> ${externalScript[1]}`,
`Web Vue 组件禁止使用外部 script src:${path.basename(sourcePath)} -> ${externalScript}`,
);
}
// 边界门禁只需解析 SFC 的脚本块;模板和样式不会产生 TypeScript 模块依赖。
return [...sourceText.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)]
.map((match) => match[1])
.join('\n');
if (errors.length > 0) {
throw new Error(`Vue 单文件组件解析失败:${path.basename(sourcePath)}`);
}
// 使用 Vue 官方解析器读取普通脚本和 setup 脚本,避免 HTML 属性写法绕过正则门禁。
return [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join('\n');
}
function collectModuleSpecifiers(sourceFile) {
@@ -108,19 +108,25 @@ const value: Internal | undefined = undefined;
assert.match(result.stderr, /IllegalView\.vue/);
});
test('拒绝 Vue 外部 script src 绕过源码边界', async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/ExternalScript.vue'),
`<script setup lang="ts" src="../../server/libs/internal.ts"></script>\n<template><main /></template>\n`,
);
for (const [name, scriptTag] of [
['双引号', '<script lang="ts" src="../../server/libs/internal.ts">'],
['单引号', "<script src='../../server/libs/internal.ts' lang='ts'>"],
['无引号且属性换序', '<script lang=ts src=../../server/libs/internal.ts>'],
]) {
test(`拒绝 Vue 外部 script src 通过${name}绕过源码边界`, async () => {
const repositoryRoot = await createFixture();
await writeFile(
path.join(repositoryRoot, 'web/src/ExternalScript.vue'),
`${scriptTag}</script>\n<template><main /></template>\n`,
);
const result = await runChecker(repositoryRoot);
const result = await runChecker(repositoryRoot);
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /禁止使用外部 script src/);
assert.match(result.stderr, /ExternalScript\.vue/);
});
assert.equal(result.exitCode, 1);
assert.match(result.stderr, /禁止使用外部 script src/);
assert.match(result.stderr, /ExternalScript\.vue/);
});
}
test('接受只消费公开 OpenAPI 类型的 Web 工程', async () => {
const repositoryRoot = await createFixture();