Harden the server and add a Links page
Security headers, cross-site write refusal, atomic JSON writes, admin backup download, /healthz and Docker health check, OnlyOffice requires a JWT secret, npm test, CI audit, arm64 hub image. New Links tab for personal bookmarks, saved with per-person settings. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b0d3aec811
commit
1f8c9493b7
16 files changed
+147
-25
No files matched your search
@@ -60,6 +60,20 @@ const wait = ms => new Promise(r => setTimeout(r, ms));
|
||||
ok('the app page links its code with the current build number, so a cached old copy can never be used', home.includes(`/app.js?v=${cfg2.build}`) && home.includes(`/style.css?v=${cfg2.build}`), home.match(/app\.js[^"]*/));
|
||||
const login = await (await fetch(base + '/login')).text(); ok('so does the sign-in page', login.includes(`/login.js?v=${cfg2.build}`), login.match(/login\.js[^"]*/));
|
||||
const va = await fetch(base + `/app.js?v=${cfg2.build}`, { headers: { cookie } }); ok('the versioned address serves the file', va.status === 200 && (await va.text()).includes('addPersonSheet'), va.status);
|
||||
// hardening: headers, cross-site writes, health check, backup, links setting
|
||||
const h1 = await fetch(base + '/login'); await h1.text();
|
||||
ok('responses carry security headers', h1.headers.get('x-content-type-options') === 'nosniff' && /frame-ancestors 'self'/.test(h1.headers.get('content-security-policy') || ''), [...h1.headers]);
|
||||
const hz = await fetch(base + '/healthz'); ok('/healthz answers without signing in', hz.status === 200 && (await hz.json()).ok === true, hz.status);
|
||||
const cs = await fetch(base + '/api/tasks/lists', { method: 'POST', headers: { cookie, origin: 'https://evil.example', 'content-type': 'application/json' }, body: '{}' }); await cs.text();
|
||||
ok('a write coming from another website is refused', cs.status === 403, cs.status);
|
||||
const cs2 = await fetch(base + '/api/settings', { method: 'PUT', headers: { cookie, 'sec-fetch-site': 'cross-site', 'content-type': 'application/json' }, body: '{"key":"links","value":[]}' }); await cs2.text();
|
||||
ok('and so is one the browser marks cross-site', cs2.status === 403, cs2.status);
|
||||
const sl = await fetch(base + '/api/settings', { method: 'PUT', headers: { cookie, origin: base, 'content-type': 'application/json' }, body: JSON.stringify({ key: 'links', value: [{ id: 'a', title: 'T', url: 'https://example.com', group: '' }] }) }); await sl.text();
|
||||
const gl = await (await fetch(base + '/api/settings', { headers: { cookie } })).json();
|
||||
ok('links are saved with the settings and read back', sl.status === 200 && gl.values.links && gl.values.links[0].url === 'https://example.com', [sl.status, gl]);
|
||||
const bk = await fetch(base + '/api/backup', { headers: { cookie } }); const bb = Buffer.from(await bk.arrayBuffer());
|
||||
ok('an admin can download a backup (a gzip file)', bk.status === 200 && bb[0] === 0x1f && bb[1] === 0x8b, [bk.status, bb.length]);
|
||||
const bn = await fetch(base + '/api/backup'); await bn.text(); ok('but not without signing in', bn.status === 401, bn.status);
|
||||
ok('the server is still running after all of that', alive);
|
||||
child.kill(); fs.rmSync(dir, { recursive: true, force: true });
|
||||
if (failed) { console.log('\n--- server log ---\n' + log.split('\n').filter(l => /error|Error|not defined/.test(l)).slice(0, 12).join('\n')); }
|
||||
|
||||
Reference in new issue
Block a user