Address CodeQL findings on the OIDC auth flow

- Open redirect / client-side XSS (app-base.js): the post-login `redirect`
  read from the URL fragment was assigned straight to window.location. Add a
  same-origin guard (safeInternalPath) that rejects absolute URLs,
  protocol-relative "//host"/"/\\host", and scheme targets like
  "javascript:". Apply it in consumeTokenFragment and logInRedirect.
- Server-side defense in depth: sanitize `redirect` when storing OidcState
  and when building the callback fragment (utils/safe_redirect.js, shared +
  unit-tested).
- Missing rate limiting: throttle the unauthenticated auth endpoints
  (/login, /oidc/start, /oidc/callback) with express-rate-limit (60/IP/15m).
  Set `trust proxy: 1` so req.ip reflects the real client behind OpenResty.

Adds test/unit/safe_redirect.test.js; unit suite 77 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 13:22:12 -04:00
parent 08bef1bd00
commit cff816fa06
7 changed files with 137 additions and 10 deletions
+43
View File
@@ -0,0 +1,43 @@
'use strict';
const {describe, test} = require('node:test');
const assert = require('node:assert');
const {safeInternalPath} = require('../../utils/safe_redirect');
/**
* safeInternalPath guards the OIDC post-login redirect against open-redirect
* and script-scheme (XSS) targets. Only same-origin "/path" values pass.
*/
describe('safeInternalPath', () => {
test('allows plain same-origin paths', () => {
assert.strictEqual(safeInternalPath('/'), '/');
assert.strictEqual(safeInternalPath('/hosts'), '/hosts');
assert.strictEqual(safeInternalPath('/dns?x=1'), '/dns?x=1');
assert.strictEqual(safeInternalPath('/a/b/c#frag'), '/a/b/c#frag');
});
test('rejects absolute URLs', () => {
assert.strictEqual(safeInternalPath('https://evil.com'), '/');
assert.strictEqual(safeInternalPath('http://evil.com/x'), '/');
});
test('rejects protocol-relative and backslash host tricks', () => {
assert.strictEqual(safeInternalPath('//evil.com'), '/');
assert.strictEqual(safeInternalPath('/\\evil.com'), '/');
});
test('rejects script / data schemes', () => {
assert.strictEqual(safeInternalPath('javascript:alert(1)'), '/');
assert.strictEqual(safeInternalPath('data:text/html,<script>'), '/');
});
test('rejects non-path and non-string input', () => {
assert.strictEqual(safeInternalPath('hosts'), '/'); // no leading slash
assert.strictEqual(safeInternalPath(''), '/');
assert.strictEqual(safeInternalPath(undefined), '/');
assert.strictEqual(safeInternalPath(null), '/');
assert.strictEqual(safeInternalPath({}), '/');
});
});