fix: 加固安全输入边界并修复订阅兼容性 (#3084)

* fix: harden scoped API inputs and isolate login throttling

* fix: close script path gaps and harden security boundaries

* fix: preserve subscription aliases and avoid extension eval

* fix: allow cancelling invalid historical subscriptions

* fix: make script parent validation explicit for CodeQL

* fix: limit command execution and clarify trusted path branches

* fix: separate diff tool script access from config APIs

* fix: clean up obsolete subscription SSH credentials
This commit is contained in:
whyour
2026-10-06 18:21:54 +08:00
committed by GitHub
parent 2ab9c702e6
commit 2cab112391
33 changed files with 2468 additions and 216 deletions
+90 -1
View File
@@ -9,7 +9,7 @@ const load = require('../helpers/load-security-module.cjs');
test('HTTP authentication protects init, scopes, expired sessions and config secrets', async (t) => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-http-security-'));
for (const dir of ['config/grpc', 'scripts', 'upload', 'tmp'])
for (const dir of ['config/grpc', 'scripts', 'upload', 'tmp', 'bak'])
fs.mkdirSync(path.join(tmp, dir), { recursive: true });
fs.writeFileSync(path.join(tmp, 'config/grpc/client.key'), 'SENTINEL');
fs.writeFileSync(path.join(tmp, 'config/normal.txt'), 'normal');
@@ -40,6 +40,8 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
scriptPath: path.join(tmp, 'scripts/'),
uploadPath: path.join(tmp, 'upload'),
tmpPath: path.join(tmp, 'tmp'),
bakPath: path.join(tmp, 'bak'),
writePathList: [path.join(tmp, 'scripts')],
blackFileList: ['auth.json', 'grpc'],
baseUrl: '/panel',
};
@@ -48,6 +50,16 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
scopes: ['configs'],
tokens: [{ value: 'config-app', expiration: Date.now() / 1000 + 3600 }],
},
{
scopes: ['configs', 'scripts'],
tokens: [
{ value: 'script-config-app', expiration: Date.now() / 1000 + 3600 },
],
},
{
scopes: ['scripts'],
tokens: [{ value: 'script-app', expiration: Date.now() / 1000 + 3600 }],
},
{
scopes: ['envs'],
tokens: [{ value: 'env-app', expiration: Date.now() / 1000 + 3600 }],
@@ -68,6 +80,8 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
getToken: (r) => (r.headers.authorization || '').replace(/^Bearer /, ''),
getPlatform: () => 'desktop',
getFileContentByName: (p) => fs.promises.readFile(p, 'utf8'),
fileExist: (p) => fs.existsSync(p),
rmPath: (p) => fs.promises.rm(p),
},
'../shared/i18n': { t: (x) => x },
'../shared/store': {
@@ -75,6 +89,7 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
},
'../config/serverEnv': { serveEnv: (_req, res) => res.end() },
'../services/user': User,
'../services/script': class {},
'../data/open': {},
'../data/system': {},
'../shared/utils': {
@@ -96,6 +111,9 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
load(path.join(__dirname, '../../back/api/config.ts'), mocks).default(
router,
);
load(path.join(__dirname, '../../back/api/script.ts'), mocks).default(
router,
);
router.get('/envs', (_req, res) => res.json({ code: 200 }));
return router;
};
@@ -210,4 +228,75 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
).status,
400,
);
const scriptName = 'data/scripts/test.js';
for (const prefix of ['/open', '/panel/open']) {
const denied = await request(
`${prefix}/configs/save`,
'config-app',
'POST',
{
name: scriptName,
content: 'overwrite',
},
);
assert.equal(denied.body.code, 403);
assert.equal(fs.existsSync(path.join(tmp, 'scripts/test.js')), false);
}
assert.equal(
(
await request('/open/configs/save', 'script-config-app', 'POST', {
name: scriptName,
content: 'authorized script',
})
).body.code,
403,
);
assert.equal(fs.existsSync(path.join(tmp, 'scripts/test.js')), false);
assert.equal(
(await request(`/open/configs/detail?path=${scriptName}`, 'config-app'))
.body.code,
403,
);
assert.equal(
(
await request('/open/configs/save', 'config-app', 'POST', {
name: 'normal.txt',
content: 'normal config',
})
).body.code,
200,
);
assert.equal(
(
await request('/api/configs/save', valid, 'POST', {
name: scriptName,
content: 'owner script',
})
).body.code,
403,
);
for (const prefix of ['/open', '/panel/open']) {
assert.equal(
(
await request(`${prefix}/scripts`, 'config-app', 'POST', {
filename: 'test.js',
content: 'unauthorized',
})
).status,
401,
);
assert.equal(
(
await request(`${prefix}/scripts`, 'script-app', 'POST', {
filename: 'test.js',
content: 'authorized script',
})
).body.code,
200,
);
assert.equal(
fs.readFileSync(path.join(tmp, 'scripts/test.js'), 'utf8'),
'authorized script',
);
}
});