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';2
const test=require('node:test');3
const assert=require('node:assert/strict');4
const crypto=require('node:crypto');5
const {openDatabase}=require('../src/database');6
const {makeApp}=require('../src/server');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};8
let app,base,cookie,csrf;9
test.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
});17
test.after(async()=>{await app.close();});18
async 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})});20
}21
test('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
});25
test('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
});30
test('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
});43
test('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
});48
test('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
});59
test('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
});