Skip to content

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-111 'use strict';
b0c4351f2earchadmin2026-10-112 const test=require('node:test');
b0c4351f2earchadmin2026-10-113 const assert=require('node:assert/strict');
b0c4351f2earchadmin2026-10-114 const crypto=require('node:crypto');
b0c4351f2earchadmin2026-10-115 const {openDatabase}=require('../src/database');
b0c4351f2earchadmin2026-10-116 const {makeApp}=require('../src/server');
b0c4351f2earchadmin2026-10-117 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-118 let app,base,cookie,csrf;
b0c4351f2earchadmin2026-10-119 test.before(async()=>{
b0c4351f2earchadmin2026-10-1110  app=makeApp({db:openDatabase(':memory:'),cfg,disableMonitor:true});
b0c4351f2earchadmin2026-10-1111  await new Promise(r=>app.server.listen(0,'127.0.0.1',r));
b0c4351f2earchadmin2026-10-1112  base=`http://127.0.0.1:${app.server.address().port}`;
b0c4351f2earchadmin2026-10-1113  const r=await fetch(base+'/admin/login');
b0c4351f2earchadmin2026-10-1114  cookie=r.headers.get('set-cookie').split(';')[0];
b0c4351f2earchadmin2026-10-1115  csrf=cookie.split('=')[1];
b0c4351f2earchadmin2026-10-1116 });
b0c4351f2earchadmin2026-10-1117 test.after(async()=>{await app.close();});
b0c4351f2earchadmin2026-10-1118 async function post(headers={},token=csrf){
b0c4351f2earchadmin2026-10-1119  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-1120 }
b0c4351f2earchadmin2026-10-1121 test('canonical HTTPS Origin behind Caddy is accepted and proceeds to credentials',async()=>{
b0c4351f2earchadmin2026-10-1122  const r=await post({origin:'https://status.archforge.nl',host:'status.archforge.nl','x-forwarded-proto':'https'});
b0c4351f2earchadmin2026-10-1123  assert.equal(r.status,401);assert.match(await r.text(),/Invalid credentials/);
b0c4351f2earchadmin2026-10-1124 });
b0c4351f2earchadmin2026-10-1125 test('HTTPS default port and case normalization are allowed',async()=>{
b0c4351f2earchadmin2026-10-1126  for(const origin of ['https://status.archforge.nl:443','https://STATUS.ARCHFORGE.NL']){
b0c4351f2earchadmin2026-10-1127  const r=await post({origin});assert.equal(r.status,401,origin);
b0c4351f2earchadmin2026-10-1128  }
b0c4351f2earchadmin2026-10-1129 });
b0c4351f2earchadmin2026-10-1130 test('Origin and Referer must match configured HTTPS public origin, not proxy-supplied Host',async()=>{
b0c4351f2earchadmin2026-10-1131  for(const headers of [
b0c4351f2earchadmin2026-10-1132  {origin:'http://status.archforge.nl'},
b0c4351f2earchadmin2026-10-1133  {origin:'https://evil.example',host:'evil.example','x-forwarded-proto':'https'},
b0c4351f2earchadmin2026-10-1134  {origin:'https://status.archforge.nl.evil.example'},
b0c4351f2earchadmin2026-10-1135  {origin:'null'},
b0c4351f2earchadmin2026-10-1136  {origin:'https://status.archforge.nl',referer:'https://evil.example/login'},
b0c4351f2earchadmin2026-10-1137  {origin:'https://status.archforge.nl/login'},
b0c4351f2earchadmin2026-10-1138  {referer:'http://status.archforge.nl/admin/login'}
b0c4351f2earchadmin2026-10-1139  ]){
b0c4351f2earchadmin2026-10-1140  const r=await post(headers);assert.equal(r.status,403,JSON.stringify(headers));assert.match(await r.text(),/Invalid origin/);
b0c4351f2earchadmin2026-10-1141  }
b0c4351f2earchadmin2026-10-1142 });
b0c4351f2earchadmin2026-10-1143 test('Both normal Origin and Referer accepted; CSRF still mandatory',async()=>{
b0c4351f2earchadmin2026-10-1144  const headers={origin:'https://status.archforge.nl',referer:'https://status.archforge.nl/admin/login'};
b0c4351f2earchadmin2026-10-1145  let r=await post(headers);assert.equal(r.status,401);
b0c4351f2earchadmin2026-10-1146  r=await post(headers,'wrong-csrf');assert.equal(r.status,403);assert.match(await r.text(),/Invalid or expired form token/);
b0c4351f2earchadmin2026-10-1147 });
299229ecf6archadmin2026-10-1248 test('browser same-origin Fetch Metadata accepts privacy-rewritten Origin/Referer only with valid CSRF',async()=>{
299229ecf6archadmin2026-10-1249  for (const headers of [
299229ecf6archadmin2026-10-1250  {'sec-fetch-site':'same-origin',origin:'null'},
299229ecf6archadmin2026-10-1251  {'sec-fetch-site':'same-origin',origin:'https://status.archforge.nl',referer:'about:blank'},
299229ecf6archadmin2026-10-1252  {'sec-fetch-site':'same-origin',origin:'https://invalid.privacy-extension.example'}
299229ecf6archadmin2026-10-1253  ]){
299229ecf6archadmin2026-10-1254  let r=await post(headers);assert.equal(r.status,401,JSON.stringify(headers));
299229ecf6archadmin2026-10-1255  r=await post(headers,'incorrect-csrf');assert.equal(r.status,403,JSON.stringify(headers));
299229ecf6archadmin2026-10-1256  assert.match(await r.text(),/Invalid or expired form token/);
299229ecf6archadmin2026-10-1257  }
299229ecf6archadmin2026-10-1258 });
299229ecf6archadmin2026-10-1259 test('cross-site and same-site Fetch Metadata are blocked despite spoofed canonical origin and matching CSRF',async()=>{
299229ecf6archadmin2026-10-1260  for (const site of ['cross-site','same-site']){
299229ecf6archadmin2026-10-1261  const r=await post({'sec-fetch-site':site,origin:'https://status.archforge.nl'});
299229ecf6archadmin2026-10-1262  assert.equal(r.status,403,site);
299229ecf6archadmin2026-10-1263  assert.match(await r.text(),/Invalid origin/);
299229ecf6archadmin2026-10-1264  }
299229ecf6archadmin2026-10-1265 });