Skip to content

Repository files, collaboration, releases and project activity.

archforge-external-status / tests/origin-proxy.test.jsmain
▤ tests/origin-proxy.test.js3825 bytes
66 lines · Select a line number for a permalink
1'use strict';
2const test=require('node:test');
3const assert=require('node:assert/strict');
4const crypto=require('node:crypto');
5const {openDatabase}=require('../src/database');
6const {makeApp}=require('../src/server');
7const 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};
8let app,base,cookie,csrf;
9test.before(async()=>{
10 app=makeApp({db:openDatabase(':memory:'),cfg,disableMonitor:true});
11 await new Promise(r=>app.server.listen(0,'127.0.0.1',r));
12 base=`http://127.0.0.1:${app.server.address().port}`;
13 const r=await fetch(base+'/admin/login');
14 cookie=r.headers.get('set-cookie').split(';')[0];
15 csrf=cookie.split('=')[1];
16});
17test.after(async()=>{await app.close();});
18async function post(headers={},token=csrf){
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})});
21test('canonical HTTPS Origin behind Caddy is accepted and proceeds to credentials',async()=>{
22 const r=await post({origin:'https://status.archforge.nl',host:'status.archforge.nl','x-forwarded-proto':'https'});
23 assert.equal(r.status,401);assert.match(await r.text(),/Invalid credentials/);
24});
25test('HTTPS default port and case normalization are allowed',async()=>{
26 for(const origin of ['https://status.archforge.nl:443','https://STATUS.ARCHFORGE.NL']){
27 const r=await post({origin});assert.equal(r.status,401,origin);
28 }
29});
30test('Origin and Referer must match configured HTTPS public origin, not proxy-supplied Host',async()=>{
31 for(const headers of [
32 {origin:'http://status.archforge.nl'},
33 {origin:'https://evil.example',host:'evil.example','x-forwarded-proto':'https'},
34 {origin:'https://status.archforge.nl.evil.example'},
35 {origin:'null'},
36 {origin:'https://status.archforge.nl',referer:'https://evil.example/login'},
37 {origin:'https://status.archforge.nl/login'},
38 {referer:'http://status.archforge.nl/admin/login'}
39 ]){
40 const r=await post(headers);assert.equal(r.status,403,JSON.stringify(headers));assert.match(await r.text(),/Invalid origin/);
41 }
42});
43test('Both normal Origin and Referer accepted; CSRF still mandatory',async()=>{
44 const headers={origin:'https://status.archforge.nl',referer:'https://status.archforge.nl/admin/login'};
45 let r=await post(headers);assert.equal(r.status,401);
46 r=await post(headers,'wrong-csrf');assert.equal(r.status,403);assert.match(await r.text(),/Invalid or expired form token/);
47});
48test('browser same-origin Fetch Metadata accepts privacy-rewritten Origin/Referer only with valid CSRF',async()=>{
49 for (const headers of [
50 {'sec-fetch-site':'same-origin',origin:'null'},
51 {'sec-fetch-site':'same-origin',origin:'https://status.archforge.nl',referer:'about:blank'},
52 {'sec-fetch-site':'same-origin',origin:'https://invalid.privacy-extension.example'}
53 ]){
54 let r=await post(headers);assert.equal(r.status,401,JSON.stringify(headers));
55 r=await post(headers,'incorrect-csrf');assert.equal(r.status,403,JSON.stringify(headers));
56 assert.match(await r.text(),/Invalid or expired form token/);
57 }
58});
59test('cross-site and same-site Fetch Metadata are blocked despite spoofed canonical origin and matching CSRF',async()=>{
60 for (const site of ['cross-site','same-site']){
61 const r=await post({'sec-fetch-site':site,origin:'https://status.archforge.nl'});
62 assert.equal(r.status,403,site);
63 assert.match(await r.text(),/Invalid origin/);
64 }
65});