fix(GOV-009): 封堵外部脚本边界绕过
This commit is contained in:
@@ -11,7 +11,7 @@ docker build -f web/Dockerfile -t registry.example/a-share-web:<提交SHA> .
|
|||||||
docker build -f server/Dockerfile -t registry.example/a-share-server:<提交SHA> .
|
docker build -f server/Dockerfile -t registry.example/a-share-server:<提交SHA> .
|
||||||
```
|
```
|
||||||
|
|
||||||
把四份生产模板复制到服务器的 `/etc/a-share-analysis/`,由运维填写并设置最小权限。Web 配置在镜像构建期固化,因此 `web.env` 只作为发布参数记录,不挂载到容器;API、Scheduler、Worker 分别读取自己的外部 env 文件。所有镜像引用必须使用明确版本或内容摘要,禁止 `latest`。
|
把四份生产模板复制到服务器的 `/etc/a-share-analysis/`,由运维填写并设置最小权限。Web 配置在镜像构建期固化,因此 `web.env` 只作为发布参数记录,不挂载到容器;API、Scheduler、Worker 分别读取自己的外部 env 文件。所有生产镜像引用必须使用内容摘要,禁止 `latest` 和任何普通 tag。
|
||||||
|
|
||||||
## 发布与验证
|
## 发布与验证
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.39.5",
|
"@eslint/js": "9.39.5",
|
||||||
|
"@vue/compiler-sfc": "3.5.41",
|
||||||
"eslint": "9.39.5",
|
"eslint": "9.39.5",
|
||||||
"eslint-plugin-vue": "9.33.0",
|
"eslint-plugin-vue": "9.33.0",
|
||||||
"globals": "17.11.0",
|
"globals": "17.11.0",
|
||||||
|
|||||||
Generated
+3
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: 9.39.5
|
specifier: 9.39.5
|
||||||
version: 9.39.5
|
version: 9.39.5
|
||||||
|
'@vue/compiler-sfc':
|
||||||
|
specifier: 3.5.41
|
||||||
|
version: 3.5.41
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 9.39.5
|
specifier: 9.39.5
|
||||||
version: 9.39.5
|
version: 9.39.5
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { readdir, readFile, stat } from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import ts from 'typescript';
|
import ts from 'typescript';
|
||||||
|
import { parse } from '@vue/compiler-sfc';
|
||||||
|
|
||||||
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.vue']);
|
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.vue']);
|
||||||
const INTERNAL_MESSAGE_MARKERS = ['MessageEnvelope', 'message-contracts'];
|
const INTERNAL_MESSAGE_MARKERS = ['MessageEnvelope', 'message-contracts'];
|
||||||
@@ -20,16 +21,18 @@ async function collectSourceFiles(directory) {
|
|||||||
|
|
||||||
function extractTypeScriptSource(sourcePath, sourceText) {
|
function extractTypeScriptSource(sourcePath, sourceText) {
|
||||||
if (path.extname(sourcePath) !== '.vue') return 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) {
|
if (externalScript) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Web Vue 组件禁止使用外部 script src:${path.basename(sourcePath)} -> ${externalScript[1]}`,
|
`Web Vue 组件禁止使用外部 script src:${path.basename(sourcePath)} -> ${externalScript}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 边界门禁只需解析 SFC 的脚本块;模板和样式不会产生 TypeScript 模块依赖。
|
if (errors.length > 0) {
|
||||||
return [...sourceText.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)]
|
throw new Error(`Vue 单文件组件解析失败:${path.basename(sourcePath)}`);
|
||||||
.map((match) => match[1])
|
}
|
||||||
.join('\n');
|
// 使用 Vue 官方解析器读取普通脚本和 setup 脚本,避免 HTML 属性写法绕过正则门禁。
|
||||||
|
return [descriptor.script?.content, descriptor.scriptSetup?.content].filter(Boolean).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectModuleSpecifiers(sourceFile) {
|
function collectModuleSpecifiers(sourceFile) {
|
||||||
|
|||||||
@@ -108,19 +108,25 @@ const value: Internal | undefined = undefined;
|
|||||||
assert.match(result.stderr, /IllegalView\.vue/);
|
assert.match(result.stderr, /IllegalView\.vue/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('拒绝 Vue 外部 script src 绕过源码边界', async () => {
|
for (const [name, scriptTag] of [
|
||||||
const repositoryRoot = await createFixture();
|
['双引号', '<script lang="ts" src="../../server/libs/internal.ts">'],
|
||||||
await writeFile(
|
['单引号', "<script src='../../server/libs/internal.ts' lang='ts'>"],
|
||||||
path.join(repositoryRoot, 'web/src/ExternalScript.vue'),
|
['无引号且属性换序', '<script lang=ts src=../../server/libs/internal.ts>'],
|
||||||
`<script setup lang="ts" src="../../server/libs/internal.ts"></script>\n<template><main /></template>\n`,
|
]) {
|
||||||
);
|
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.equal(result.exitCode, 1);
|
||||||
assert.match(result.stderr, /禁止使用外部 script src/);
|
assert.match(result.stderr, /禁止使用外部 script src/);
|
||||||
assert.match(result.stderr, /ExternalScript\.vue/);
|
assert.match(result.stderr, /ExternalScript\.vue/);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('接受只消费公开 OpenAPI 类型的 Web 工程', async () => {
|
test('接受只消费公开 OpenAPI 类型的 Web 工程', async () => {
|
||||||
const repositoryRoot = await createFixture();
|
const repositoryRoot = await createFixture();
|
||||||
|
|||||||
Reference in New Issue
Block a user