mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-15 19:57:07 +08:00
fix: harden authentication and file access security
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
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'])
|
||||
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');
|
||||
const secret = 'http-security-test';
|
||||
const valid = jwt.sign({}, secret, { algorithm: 'HS384', expiresIn: '1h' });
|
||||
const expired = jwt.sign({}, secret, { algorithm: 'HS384', expiresIn: -1 });
|
||||
const auth = {
|
||||
username: 'owner',
|
||||
password: 'configured',
|
||||
token: valid,
|
||||
tokens: { desktop: [{ value: expired }] },
|
||||
};
|
||||
const configSource = fs.readFileSync(
|
||||
path.join(__dirname, '../../back/config/index.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const whitelistSource = configSource
|
||||
.slice(configSource.indexOf('apiWhiteList:'))
|
||||
.match(/apiWhiteList:\s*\[([\s\S]*?)\]/)[1];
|
||||
const config = {
|
||||
api: { prefix: '/api' },
|
||||
apiWhiteList: [...whitelistSource.matchAll(/['"]([^'"]+)['"]/g)].map(
|
||||
(x) => x[1],
|
||||
),
|
||||
jwt: { secret },
|
||||
rootPath: tmp,
|
||||
configPath: path.join(tmp, 'config/'),
|
||||
scriptPath: path.join(tmp, 'scripts/'),
|
||||
uploadPath: path.join(tmp, 'upload'),
|
||||
tmpPath: path.join(tmp, 'tmp'),
|
||||
blackFileList: ['auth.json', 'grpc'],
|
||||
baseUrl: '/panel',
|
||||
};
|
||||
const apps = [
|
||||
{
|
||||
scopes: ['configs'],
|
||||
tokens: [{ value: 'config-app', expiration: Date.now() / 1000 + 3600 }],
|
||||
},
|
||||
{
|
||||
scopes: ['envs'],
|
||||
tokens: [{ value: 'env-app', expiration: Date.now() / 1000 + 3600 }],
|
||||
},
|
||||
];
|
||||
let initialized = 0;
|
||||
const User = class {};
|
||||
const user = {
|
||||
initializeUser: async () => {
|
||||
initialized++;
|
||||
return { code: 200 };
|
||||
},
|
||||
getAuthInfo: async () => auth,
|
||||
};
|
||||
const mocks = {
|
||||
'../config': config,
|
||||
'../config/util': {
|
||||
getToken: (r) => (r.headers.authorization || '').replace(/^Bearer /, ''),
|
||||
getPlatform: () => 'desktop',
|
||||
getFileContentByName: (p) => fs.promises.readFile(p, 'utf8'),
|
||||
},
|
||||
'../shared/i18n': { t: (x) => x },
|
||||
'../shared/store': {
|
||||
shareStore: { getAuthInfo: async () => auth, getApps: async () => apps },
|
||||
},
|
||||
'../config/serverEnv': { serveEnv: (_req, res) => res.end() },
|
||||
'../services/user': User,
|
||||
'../data/open': {},
|
||||
'../data/system': {},
|
||||
'../shared/utils': {
|
||||
writeFileWithLock: (p, content) => fs.promises.writeFile(p, content),
|
||||
},
|
||||
};
|
||||
const Config = load(path.join(__dirname, '../../back/services/config.ts'), {
|
||||
...mocks,
|
||||
typedi: { Service: () => (x) => x },
|
||||
}).default;
|
||||
const configService = new Config();
|
||||
mocks['../services/config'] = Config;
|
||||
mocks.typedi = {
|
||||
Container: { get: (x) => (x === User ? user : configService) },
|
||||
};
|
||||
mocks['../api'] = () => {
|
||||
const router = express.Router();
|
||||
load(path.join(__dirname, '../../back/api/user.ts'), mocks).default(router);
|
||||
load(path.join(__dirname, '../../back/api/config.ts'), mocks).default(
|
||||
router,
|
||||
);
|
||||
router.get('/envs', (_req, res) => res.json({ code: 200 }));
|
||||
return router;
|
||||
};
|
||||
const app = express();
|
||||
load(path.join(__dirname, '../../back/loaders/express.ts'), mocks).default({
|
||||
app,
|
||||
});
|
||||
const server = app.listen(0, '127.0.0.1');
|
||||
t.after(async () => {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.on('listening', resolve);
|
||||
server.on('error', reject);
|
||||
});
|
||||
const request = async (url, token, method = 'GET', body) => {
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${server.address().port}${url}`,
|
||||
{
|
||||
method,
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
},
|
||||
);
|
||||
return { status: response.status, body: await response.json() };
|
||||
};
|
||||
for (const url of [
|
||||
'/api/user/init',
|
||||
'/open/user/init',
|
||||
'/panel/api/user/init',
|
||||
'/panel/open/user/init',
|
||||
]) {
|
||||
const response = await request(url, undefined, 'PUT', {
|
||||
username: 'attacker',
|
||||
password: 'changed',
|
||||
});
|
||||
assert.equal(response.body.code, 450, url);
|
||||
}
|
||||
assert.equal(initialized, 0);
|
||||
assert.equal(
|
||||
(
|
||||
await request('/api/user/login', undefined, 'POST', {
|
||||
username: 'x',
|
||||
password: 'x'.repeat(20000),
|
||||
})
|
||||
).status,
|
||||
413,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, 'upload/legacy.html'),
|
||||
'<script>window.test=1</script>',
|
||||
);
|
||||
const legacyUpload = await fetch(
|
||||
`http://127.0.0.1:${server.address().port}/api/static/legacy.html`,
|
||||
);
|
||||
assert.equal(legacyUpload.headers.get('x-content-type-options'), 'nosniff');
|
||||
assert.equal(
|
||||
legacyUpload.headers.get('content-security-policy'),
|
||||
"sandbox; default-src 'none'",
|
||||
);
|
||||
for (const prefix of ['/api', '/open', '/panel/api', '/panel/open']) {
|
||||
assert.equal(
|
||||
(await request(`${prefix}/envs`, expired)).status,
|
||||
401,
|
||||
prefix,
|
||||
);
|
||||
assert.equal(
|
||||
(await request(`${prefix}/envs`, valid)).body.code,
|
||||
200,
|
||||
prefix,
|
||||
);
|
||||
assert.equal((await request(`${prefix}/envs`)).status, 401, prefix);
|
||||
}
|
||||
assert.equal((await request('/open/envs', 'env-app')).body.code, 200);
|
||||
assert.equal(
|
||||
(await request('/open/configs/detail?path=normal.txt', 'env-app')).status,
|
||||
401,
|
||||
);
|
||||
assert.equal(
|
||||
(await request('/open/configs/detail?path=normal.txt', 'config-app')).body
|
||||
.data,
|
||||
'normal',
|
||||
);
|
||||
assert.equal(
|
||||
(await request('/open/configs/detail?path=grpc/client.key', 'config-app'))
|
||||
.body.code,
|
||||
403,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await request('/open/configs/save', 'config-app', 'POST', {
|
||||
name: 'grpc/client.key',
|
||||
content: 'changed',
|
||||
})
|
||||
).body.code,
|
||||
403,
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(tmp, 'config/grpc/client.key'), 'utf8'),
|
||||
'SENTINEL',
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await request('/Api/user/init', undefined, 'PUT', {
|
||||
username: 'x',
|
||||
password: 'y',
|
||||
})
|
||||
).status,
|
||||
400,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user