security + reponsive
This commit is contained in:
97
test/security.test.js
Normal file
97
test/security.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.DATA_ENCRYPTION_SECRET = 'test-only-encryption-secret-with-more-than-32-characters';
|
||||
|
||||
const {
|
||||
app,
|
||||
encryptSensitiveValue,
|
||||
decryptSensitiveValue,
|
||||
hashSessionToken,
|
||||
normalizeOptionalHttpUrl
|
||||
} = require('../backend/server');
|
||||
|
||||
async function withTestServer(run) {
|
||||
const server = app.listen(0, '127.0.0.1');
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('listening', resolve);
|
||||
server.once('error', reject);
|
||||
});
|
||||
|
||||
try {
|
||||
const address = server.address();
|
||||
await run(`http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
test('stored credentials use authenticated encryption and round-trip safely', () => {
|
||||
const plainText = 'example-password-value';
|
||||
const first = encryptSensitiveValue(plainText);
|
||||
const second = encryptSensitiveValue(plainText);
|
||||
|
||||
assert.match(first, /^enc:v2:/);
|
||||
assert.notEqual(first, second);
|
||||
assert.equal(first.includes(plainText), false);
|
||||
assert.equal(decryptSensitiveValue(first), plainText);
|
||||
assert.equal(decryptSensitiveValue(`${first}tampered`), null);
|
||||
});
|
||||
|
||||
test('session tokens are stored as deterministic SHA-256 hashes', () => {
|
||||
const token = 'a-private-random-session-token';
|
||||
const hash = hashSessionToken(token);
|
||||
|
||||
assert.match(hash, /^[a-f0-9]{64}$/);
|
||||
assert.equal(hash, hashSessionToken(token));
|
||||
assert.equal(hash.includes(token), false);
|
||||
});
|
||||
|
||||
test('application URLs accept only HTTP and HTTPS protocols', () => {
|
||||
assert.equal(normalizeOptionalHttpUrl(''), '');
|
||||
assert.equal(normalizeOptionalHttpUrl('javascript:alert(1)'), null);
|
||||
assert.equal(normalizeOptionalHttpUrl('file:///etc/passwd'), null);
|
||||
assert.match(normalizeOptionalHttpUrl('https://example.com/path'), /^https:\/\/example\.com\/path/);
|
||||
});
|
||||
|
||||
test('forged legacy identity headers cannot bypass protected APIs', async () => {
|
||||
await withTestServer(async baseUrl => {
|
||||
const response = await fetch(`${baseUrl}/api/users`, {
|
||||
headers: {
|
||||
'x-user-id': '1',
|
||||
'x-user-role': 'admin'
|
||||
}
|
||||
});
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.success, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('security headers and no-store API caching are enabled', async () => {
|
||||
await withTestServer(async baseUrl => {
|
||||
const response = await fetch(`${baseUrl}/api/health`);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.headers.get('content-security-policy') || '', /default-src 'self'/);
|
||||
assert.equal(response.headers.get('x-powered-by'), null);
|
||||
assert.equal(response.headers.get('cache-control'), 'no-store');
|
||||
});
|
||||
});
|
||||
|
||||
test('state-changing requests from untrusted origins are rejected before route handling', async () => {
|
||||
await withTestServer(async baseUrl => {
|
||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Origin: 'https://attacker.example',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username: 'test', password: 'not-a-real-password' })
|
||||
});
|
||||
|
||||
assert.equal(response.status, 403);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user