Repository files, collaboration, releases and project activity.
ARCHFORGE HISTORY
Blame · tests/origin-proxy.test.js
Branch main · the Commit author is not necessarily a verified ArchForge account.
b0c4351f2earchadmin2026-10-11
1 'use strict';b0c4351f2earchadmin2026-10-11
2 const test=require('node:test');b0c4351f2earchadmin2026-10-11
3 const assert=require('node:assert/strict');b0c4351f2earchadmin2026-10-11
4 const crypto=require('node:crypto');b0c4351f2earchadmin2026-10-11
5 const {openDatabase}=require('../src/database');b0c4351f2earchadmin2026-10-11
6 const {makeApp}=require('../src/server');b0c4351f2earchadmin2026-10-11
7 const cfg={key:crypto.randomBytes(32).toString('hex'),dbPath:':memory:',publicUrl:'https://status.archforge.nl/',website:'https://archforge.nl/',websiteHost:'archforge.nl',api:'',gitHttp:'',gitHost:'archforge.nl',gitPort:22,discord:'',smtpAvailable:false,smtpFrom:'',failures:3,recoveries:2,timeout:2000,interval:60000,host:'127.0.0.1',port:0,test:true};b0c4351f2earchadmin2026-10-11
8 let app,base,cookie,csrf;b0c4351f2earchadmin2026-10-11
9 test.before(async()=>{b0c4351f2earchadmin2026-10-11
10 app=makeApp({db:openDatabase(':memory:'),cfg,disableMonitor:true});b0c4351f2earchadmin2026-10-11
11 await new Promise(r=>app.server.listen(0,'127.0.0.1',r));b0c4351f2earchadmin2026-10-11
12 base=`http://127.0.0.1:${app.server.address().port}`;b0c4351f2earchadmin2026-10-11
13 const r=await fetch(base+'/admin/login');b0c4351f2earchadmin2026-10-11
14 cookie=r.headers.get('set-cookie').split(';')[0];b0c4351f2earchadmin2026-10-11
15 csrf=cookie.split('=')[1];b0c4351f2earchadmin2026-10-11
16 });b0c4351f2earchadmin2026-10-11
17 test.after(async()=>{await app.close();});b0c4351f2earchadmin2026-10-11
18 async function post(headers={},token=csrf){b0c4351f2earchadmin2026-10-11
19 return fetch(base+'/admin/login',{method:'POST',redirect:'manual',headers:{cookie,'content-type':'application/x-www-form-urlencoded',...headers},body:new URLSearchParams({username:'invalid',password:'wrong',totp:'000000',csrf:token})});b0c4351f2earchadmin2026-10-11
20 }b0c4351f2earchadmin2026-10-11
21 test('canonical HTTPS Origin behind Caddy is accepted and proceeds to credentials',async()=>{b0c4351f2earchadmin2026-10-11
22 const r=await post({origin:'https://status.archforge.nl',host:'status.archforge.nl','x-forwarded-proto':'https'});b0c4351f2earchadmin2026-10-11
23 assert.equal(r.status,401);assert.match(await r.text(),/Invalid credentials/);b0c4351f2earchadmin2026-10-11
24 });b0c4351f2earchadmin2026-10-11
25 test('HTTPS default port and case normalization are allowed',async()=>{b0c4351f2earchadmin2026-10-11
26 for(const origin of ['https://status.archforge.nl:443','https://STATUS.ARCHFORGE.NL']){b0c4351f2earchadmin2026-10-11
27 const r=await post({origin});assert.equal(r.status,401,origin);b0c4351f2earchadmin2026-10-11
28 }b0c4351f2earchadmin2026-10-11
29 });b0c4351f2earchadmin2026-10-11
30 test('Origin and Referer must match configured HTTPS public origin, not proxy-supplied Host',async()=>{b0c4351f2earchadmin2026-10-11
31 for(const headers of [b0c4351f2earchadmin2026-10-11
32 {origin:'http://status.archforge.nl'},b0c4351f2earchadmin2026-10-11
33 {origin:'https://evil.example',host:'evil.example','x-forwarded-proto':'https'},b0c4351f2earchadmin2026-10-11
34 {origin:'https://status.archforge.nl.evil.example'},b0c4351f2earchadmin2026-10-11
35 {origin:'null'},b0c4351f2earchadmin2026-10-11
36 {origin:'https://status.archforge.nl',referer:'https://evil.example/login'},b0c4351f2earchadmin2026-10-11
37 {origin:'https://status.archforge.nl/login'},b0c4351f2earchadmin2026-10-11
38 {referer:'http://status.archforge.nl/admin/login'}b0c4351f2earchadmin2026-10-11
39 ]){b0c4351f2earchadmin2026-10-11
40 const r=await post(headers);assert.equal(r.status,403,JSON.stringify(headers));assert.match(await r.text(),/Invalid origin/);b0c4351f2earchadmin2026-10-11
41 }b0c4351f2earchadmin2026-10-11
42 });b0c4351f2earchadmin2026-10-11
43 test('Both normal Origin and Referer accepted; CSRF still mandatory',async()=>{b0c4351f2earchadmin2026-10-11
44 const headers={origin:'https://status.archforge.nl',referer:'https://status.archforge.nl/admin/login'};b0c4351f2earchadmin2026-10-11
45 let r=await post(headers);assert.equal(r.status,401);b0c4351f2earchadmin2026-10-11
46 r=await post(headers,'wrong-csrf');assert.equal(r.status,403);assert.match(await r.text(),/Invalid or expired form token/);b0c4351f2earchadmin2026-10-11
47 });299229ecf6archadmin2026-10-12
48 test('browser same-origin Fetch Metadata accepts privacy-rewritten Origin/Referer only with valid CSRF',async()=>{299229ecf6archadmin2026-10-12
49 for (const headers of [299229ecf6archadmin2026-10-12
50 {'sec-fetch-site':'same-origin',origin:'null'},299229ecf6archadmin2026-10-12
51 {'sec-fetch-site':'same-origin',origin:'https://status.archforge.nl',referer:'about:blank'},299229ecf6archadmin2026-10-12
52 {'sec-fetch-site':'same-origin',origin:'https://invalid.privacy-extension.example'}299229ecf6archadmin2026-10-12
53 ]){299229ecf6archadmin2026-10-12
54 let r=await post(headers);assert.equal(r.status,401,JSON.stringify(headers));299229ecf6archadmin2026-10-12
55 r=await post(headers,'incorrect-csrf');assert.equal(r.status,403,JSON.stringify(headers));299229ecf6archadmin2026-10-12
56 assert.match(await r.text(),/Invalid or expired form token/);299229ecf6archadmin2026-10-12
57 }299229ecf6archadmin2026-10-12
58 });299229ecf6archadmin2026-10-12
59 test('cross-site and same-site Fetch Metadata are blocked despite spoofed canonical origin and matching CSRF',async()=>{299229ecf6archadmin2026-10-12
60 for (const site of ['cross-site','same-site']){299229ecf6archadmin2026-10-12
61 const r=await post({'sec-fetch-site':site,origin:'https://status.archforge.nl'});299229ecf6archadmin2026-10-12
62 assert.equal(r.status,403,site);299229ecf6archadmin2026-10-12
63 assert.match(await r.text(),/Invalid origin/);299229ecf6archadmin2026-10-12
64 }299229ecf6archadmin2026-10-12
65 });