Merge pull request #5386 from mikiher/fix/oidc-url-validation

Fix oidc url validation
This commit is contained in:
advplyr 2026-07-20 18:08:51 -04:00 committed by GitHub
commit 528d03fef3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 193 additions and 41 deletions

View file

@ -0,0 +1,77 @@
const { expect } = require('chai')
const sinon = require('sinon')
// Load Database first so Auth resolves OidcAuthStrategy before the circular require completes.
require('../../../server/Database')
const OidcAuthStrategy = require('../../../server/auth/OidcAuthStrategy')
const Logger = require('../../../server/Logger')
describe('OidcAuthStrategy - isValidWebCallbackUrl', () => {
/** @type {OidcAuthStrategy} */
let strategy
beforeEach(() => {
global.RouterBasePath = ''
strategy = new OidcAuthStrategy()
sinon.stub(Logger, 'warn')
sinon.stub(Logger, 'error')
})
afterEach(() => {
sinon.restore()
})
function mockReq({ secure = false, host = 'books.example.com', xForwardedProto = null } = {}) {
return {
secure,
get(header) {
if (header === 'host') return host
if (header === 'x-forwarded-proto') return xForwardedProto
return null
}
}
}
it('accepts a same-origin relative path when router base path is empty', () => {
expect(strategy.isValidWebCallbackUrl('/library', mockReq())).to.equal(true)
})
it('accepts a same-origin absolute https URL', () => {
const req = mockReq({ secure: true })
expect(strategy.isValidWebCallbackUrl('https://books.example.com/library', req)).to.equal(true)
})
it('rejects protocol-relative URLs', () => {
expect(strategy.isValidWebCallbackUrl('//evil.example/capture', mockReq())).to.equal(false)
})
it('rejects backslash-prefixed URLs', () => {
expect(strategy.isValidWebCallbackUrl('/\\evil.example/capture', mockReq())).to.equal(false)
})
it('rejects absolute external URLs', () => {
expect(strategy.isValidWebCallbackUrl('http://evil.example/capture', mockReq())).to.equal(false)
})
it('rejects encoded protocol-relative path segments', () => {
expect(strategy.isValidWebCallbackUrl('/%2F%2Fevil.example/capture', mockReq())).to.equal(false)
})
it('rejects same-origin URLs outside router base path', () => {
global.RouterBasePath = '/audiobookshelf'
expect(strategy.isValidWebCallbackUrl('/login', mockReq())).to.equal(false)
expect(strategy.isValidWebCallbackUrl('/audiobookshelf/login', mockReq())).to.equal(true)
})
it('rejects empty and malformed callback URLs', () => {
expect(strategy.isValidWebCallbackUrl('', mockReq())).to.equal(false)
expect(strategy.isValidWebCallbackUrl(null, mockReq())).to.equal(false)
expect(strategy.isValidWebCallbackUrl('not a url', mockReq())).to.equal(false)
})
it('uses x-forwarded-proto when determining same-origin https URLs', () => {
const req = mockReq({ xForwardedProto: 'https' })
expect(strategy.isValidWebCallbackUrl('https://books.example.com/login', req)).to.equal(true)
expect(strategy.isValidWebCallbackUrl('http://books.example.com/login', req)).to.equal(false)
})
})

View file

@ -0,0 +1,40 @@
const { expect } = require('chai')
const { isRequestSecure, getRequestProtocol, getRequestOrigin } = require('../../../server/utils/requestUtils')
function mockReq({ secure = false, host = 'books.example.com', xForwardedProto = null } = {}) {
return {
secure,
get(header) {
if (header === 'host') return host
if (header === 'x-forwarded-proto') return xForwardedProto
return null
}
}
}
describe('requestUtils', () => {
it('isRequestSecure uses req.secure', () => {
expect(isRequestSecure(mockReq({ secure: true }))).to.equal(true)
expect(isRequestSecure(mockReq({ secure: false }))).to.equal(false)
})
it('isRequestSecure uses x-forwarded-proto', () => {
expect(isRequestSecure(mockReq({ xForwardedProto: 'https' }))).to.equal(true)
expect(isRequestSecure(mockReq({ xForwardedProto: 'http' }))).to.equal(false)
expect(isRequestSecure(mockReq({ xForwardedProto: 'http, https' }))).to.equal(true)
})
it('getRequestProtocol returns https or http', () => {
expect(getRequestProtocol(mockReq({ secure: true }))).to.equal('https')
expect(getRequestProtocol(mockReq())).to.equal('http')
})
it('getRequestOrigin builds origin from protocol and host', () => {
expect(getRequestOrigin(mockReq({ secure: true }))).to.deep.equal({
protocol: 'https',
host: 'books.example.com',
origin: 'https://books.example.com'
})
})
})