// Reconstruct ONLY the RustProxy 0.31.4 administration Origin guard decision. // Real-source anchors guard against accidental model/source drift. This is NOT // a Rust server, not a browser, and not an HTTP/2 or TLS integration test. import assert from 'node:assert/strict'; import fs from 'node:fs'; import http from 'node:http'; import {fileURLToPath} from 'node:url'; import path from 'node:path'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const read = p => fs.readFileSync(path.join(root, p), 'utf8'); const source = { settings: read('src/settings.rs') + read('src/settings/public_origin.rs'), access: read('src/admin/access.rs'), session: read('src/admin/session.rs'), forwarding: read('src/forwarding.rs'), }; for (const [part, tokens] of Object.entries({ settings: ['pub public_origin: Option', 'fn include_public_origin_host(&mut self)', 'self.public_origin != active.public_origin'], access: ['fn configured_admin_public_authority(', 'fn pinned_admin_proto_compatible(', 'hops > 8', 'scheme != "https"', 'trusted_admin_proxy(state, peer)'], session: ['configured_admin_public_authority(&state, headers, peer, &raw_host, transport)', 'automation_credentials', 'pinned_origin_mismatch', '"origin_mismatch"', '"cross_site_fetch"'], forwarding: ['self.trust_proto && self.trusted(ip)'], })) for (const token of tokens) assert(source[part].includes(token), `Source contract missing: ${part}/${token}`); const defaults = {host:'127.0.0.1:9090', 'x-forwarded-host':'panel.example.test', 'x-forwarded-proto':'https', 'sec-fetch-site':'same-origin'}; const context = {listenIp:'127.0.0.1', listenPort:9090, tls:false, peerTrusted:true, trustProto:true, publicOrigin:null, allowedHosts:['panel.example.test'], automation:false}; const normal = x => x.toLowerCase(); function authority(value) { if (typeof value !== 'string' || !value || value.includes('@') || value.includes(',') || value.includes('/')) return null; try { const parsed = new URL(`https://${value}/`); if (parsed.username || parsed.password || parsed.hash || parsed.search || parsed.pathname !== '/') return null; const host = parsed.hostname.toLowerCase(); return {host, port:parsed.port ? Number(parsed.port) : null}; } catch {return null;} } function sameAuthority(a,b,scheme) { const left=authority(a), right=authority(b); if (!left || !right) return false; const defaultPort = scheme === 'https' ? 443 : 80; return left.host===right.host && (left.port ?? defaultPort)===(right.port ?? defaultPort); } function hostAllowed(value, tls, cfg) { const actual=authority(value); if (!actual) return false; const port=actual.port ?? (tls ? 443 : 80); const pinnedHost=cfg.publicOrigin ? new URL(cfg.publicOrigin).hostname.toLowerCase() : null; // The validated public panel hostname is derived at runtime. return (actual.host===cfg.listenIp && port===cfg.listenPort) || cfg.allowedHosts.includes(actual.host) || actual.host===pinnedHost; } const map = raw => { const headers=new Map(); for (let i=0;i headers.get(name)||[]; const single = (headers,name) => all(headers,name).length===1 ? all(headers,name)[0] : null; function protoCompatible(headers) { let hops=0; for (const value of all(headers,'x-forwarded-proto')) { for (const part of value.split(',')) { hops++; const scheme=part.trim(); if (hops>8 || (hops===1 && scheme!=='https') || !['http','https'].includes(scheme)) return false; } } return true; } function originMatches(value,scheme,external) { if (typeof value!=='string' || value.includes('#') || value.includes('@')) return false; try { const uri=new URL(value); if (uri.protocol!==`${scheme}:` || uri.pathname!=='/' || uri.search || uri.hash || uri.username || uri.password) return false; return sameAuthority(uri.host, external, scheme); } catch {return false;} } function guard(headers,cfg) { const rawHost=single(headers,'host'); if (!rawHost) return {status:403,reason:'invalid_host'}; let scheme=cfg.tls ? 'https' : 'http', external=rawHost; // 0.31.4 public pin only behind a trusted last hop and only for panel traffic. if (cfg.publicOrigin && cfg.peerTrusted && !cfg.automation) { let pinned; try { pinned=new URL(cfg.publicOrigin); } catch {return {status:403,reason:'pinned_origin_mismatch'};} if (pinned.protocol!=='https:' || !hostAllowed(rawHost,cfg.tls,cfg) || !protoCompatible(headers)) return {status:403,reason:'pinned_origin_mismatch'}; const xfh=all(headers,'x-forwarded-host'); if (xfh.length>1 || xfh.length===1 && !sameAuthority(xfh[0],pinned.host,'https')) return {status:403,reason:'pinned_origin_mismatch'}; scheme='https'; external=pinned.host; } else { if (!cfg.tls && cfg.peerTrusted && cfg.trustProto && all(headers,'x-forwarded-proto').length) { const proto=single(headers,'x-forwarded-proto'); if (proto!=='https' && proto!=='http') return {status:403,reason:'external_scheme'}; scheme=proto; } if (cfg.peerTrusted && all(headers,'x-forwarded-host').length) external=single(headers,'x-forwarded-host'); if (!hostAllowed(external,scheme==='https',cfg)) return {status:403,reason:'invalid_host'}; } const originValues=all(headers,'origin'); if (originValues.length>1 || originValues.length===1 && !originMatches(originValues[0],scheme,external)) return {status:403,reason:'origin_mismatch'}; if (all(headers,'sec-fetch-site').includes('cross-site')) return {status:403,reason:'cross_site_fetch'}; return {status:200,reason:'pass',scheme}; } const cases=[]; const add=(name,patch,opts,expected)=>cases.push({name,patch,opts,expected}); const PIN={publicOrigin:'https://panel.example.test',allowedHosts:[]}; add('legacy normal same-origin GET', {},{},'pass'); add('legacy malformed multi-hop chain', {'x-forwarded-proto':'https, http'},{},'external_scheme'); add('legacy duplicated XFP lines', {'x-forwarded-proto':['https','http']},{},'external_scheme'); add('pin: normal GET', {},PIN,'pass'); add('pin: GET multi-hop https then http',{'x-forwarded-proto':'https, http'},PIN,'pass'); add('pin: duplicate XFP lines',{'x-forwarded-proto':['https','http']},PIN,'pass'); add('pin: 8-hop bounded chain',{'x-forwarded-proto':'https, http, http, http, http, http, http, http'},PIN,'pass'); add('pin: 9-hop rejected',{'x-forwarded-proto':'https, http, http, http, http, http, http, http, http'},PIN,'pinned_origin_mismatch'); add('pin: no XFP; public origin configured',{'x-forwarded-proto':null},PIN,'pass'); add('pin: no XFH; internal Host still validated',{'x-forwarded-host':null,'origin':'https://panel.example.test'},PIN,'pass'); add('pin: wrong XFH',{'x-forwarded-host':'attacker.example.test'},PIN,'pinned_origin_mismatch'); add('pin: different allowed Host but wrong configured URL',{'x-forwarded-host':'panel.example.test:8443'},PIN,'pinned_origin_mismatch'); add('pin: XFH userinfo rejected',{'x-forwarded-host':'attacker@panel.example.test'},PIN,'pinned_origin_mismatch'); add('pin: duplicate XFH lines',{'x-forwarded-host':['panel.example.test','panel.example.test']},PIN,'pinned_origin_mismatch'); add('pin: first hop downgraded HTTP',{'x-forwarded-proto':'http, https'},PIN,'pinned_origin_mismatch'); add('pin: HTTP only',{'x-forwarded-proto':'http'},PIN,'pinned_origin_mismatch'); add('pin: unrecognized protocol',{'x-forwarded-proto':'ftp, http'},PIN,'pinned_origin_mismatch'); add('pin: empty protocol element',{'x-forwarded-proto':'https, '},PIN,'pinned_origin_mismatch'); add('pin: invalid XFP capitalization',{'x-forwarded-proto':'HTTPS'},PIN,'pinned_origin_mismatch'); add('pin: correct explicit browser Origin',{'origin':'https://panel.example.test','x-forwarded-proto':'https, http'},PIN,'pass'); add('pin: correct explicit Origin with :443',{'origin':'https://panel.example.test:443'},PIN,'pass'); add('pin: attacker Origin rejected',{'origin':'https://attacker.example.test'},PIN,'origin_mismatch'); add('pin: Origin null rejected',{'origin':'null'},PIN,'origin_mismatch'); add('pin: conflicting Origin port rejected',{'origin':'https://panel.example.test:8443'},PIN,'origin_mismatch'); add('pin: cross-site API request rejected',{'sec-fetch-site':'cross-site'},PIN,'cross_site_fetch'); add('pin: untrusted peer must not apply pin',{'x-forwarded-proto':'https, http','origin':'https://panel.example.test'}, {...PIN,peerTrusted:false},'origin_mismatch'); add('pin: credential automation remains strict',{'x-forwarded-proto':'https, http'}, {...PIN,automation:true},'external_scheme'); add('pin: Host cannot spoof unallowlisted identity',{'host':'attacker.example.test'},PIN,'pinned_origin_mismatch'); add('pin: correct alt port 8443',{'x-forwarded-host':'panel.example.test:8443','origin':'https://panel.example.test:8443'}, {...PIN, publicOrigin:'https://panel.example.test:8443'},'pass'); function encodedHeaders(patch) { const merged={...defaults,...patch}; const result={}; for (const [key,value] of Object.entries(merged)) if (value!==null) result[key]=value; return result; } const pending=[]; const server=http.createServer((req,res)=>{ const item=pending.shift(); if (!item) return res.writeHead(500).end('no scenario'); const result=guard(map(req.rawHeaders),{...context,...item.opts}); res.writeHead(result.status, {'content-type':'application/json'}).end(JSON.stringify(result)); }); await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); async function request(item) { pending.push(item); return await new Promise((resolve,reject)=>{ const req=http.request({hostname:'127.0.0.1',port:server.address().port,path:'/api/auth/session', agent:false,headers:encodedHeaders(item.patch)},res=>{ let data='';res.setEncoding('utf8');res.on('data',part=>data+=part); res.on('end',()=>resolve(JSON.parse(data))); }); req.on('error',reject);req.end(); }); } try { for (const item of cases) { const result=await request(item); assert.equal(result.reason,item.expected,item.name); assert.equal(result.status,item.expected==='pass' ? 200:403,item.name); } const repeats=[]; for (const configured of [false,true]) { for (let attempt=1;attempt<=12;attempt++) { const malformed=attempt%4===0; const result=await request({patch:{'x-forwarded-proto':malformed?'https, http':'https'}, opts:configured? PIN : {}}); assert.equal(result.status,configured?200:(malformed?403:200)); repeats.push({pin:configured,attempt,forwardedProto:malformed?'https, http':'https',status:result.status,reason:result.reason}); } } const summary={cases:cases.length,repeated:repeats.length,unconfigured403:repeats.filter(x=>!x.pin&&x.status===403).length, pinned403:repeats.filter(x=>x.pin&&x.status===403).length}; const output={release:'0.31.4',classification:'Node.js HTTP simulation of admin guard, not compiled RustProxy', assertions:cases.length+repeats.length,scenarios:cases.map(({name,expected})=>({name,expected})),repeats,summary}; if (process.argv[2]) fs.writeFileSync(process.argv[2],JSON.stringify(output,null,2)+'\n'); console.log(`PASS: ${output.assertions} HTTP-simulation assertions: ${summary.cases} cases + ${summary.repeated} resumes; `+ `403 with no pin ${summary.unconfigured403}/12, 403 with pin ${summary.pinned403}/12`); } finally {server.close();}