Skip to content

Commit ed93db2

Browse files
authored
Fix: HEAD requests to API routes crash auth middleware (#76)
1 parent c92bd31 commit ed93db2

4 files changed

Lines changed: 48 additions & 3 deletions

File tree

lib/AuthModule.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,12 @@ class AuthModule extends AbstractModule {
158158
} catch (e) {
159159
initError = e
160160
}
161-
const method = req.method.toLowerCase()
161+
// Treat HEAD as GET: Express runs the GET handler chain for HEAD requests,
162+
// so auth rules must follow suit.
163+
const method = req.method === 'HEAD' ? 'get' : req.method.toLowerCase()
162164
const route = `${req.baseUrl}${req.route.path}`
163165
const shortRoute = route.slice(0, route.lastIndexOf('/'))
164-
const isUnsecured = this.unsecuredRoutes[method][route] || this.unsecuredRoutes[method][shortRoute]
166+
const isUnsecured = this.unsecuredRoutes[method]?.[route] || this.unsecuredRoutes[method]?.[shortRoute]
165167

166168
if (initError && !isUnsecured) {
167169
this.log('debug', 'BLOCK_REQUEST', req.originalUrl, initError.statusCode, req?.auth?.user?._id)

lib/Permissions.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ class Permissions {
9292
async check (req) {
9393
const route = `${req.baseUrl}${req.path.endsWith('/') ? req.path.slice(0, -1) : req.path}`
9494
const userScopes = req.auth.scopes || []
95-
const neededScopes = this.getScopesForRoute(req.method.toLowerCase(), route)
95+
// HEAD reuses the GET handler chain in Express, so it must reuse GET's scopes too.
96+
const method = req.method === 'HEAD' ? 'get' : req.method.toLowerCase()
97+
const neededScopes = this.getScopesForRoute(method, route)
9698
if (!neededScopes) {
9799
log('warn', `blocked access to route with no permissions '${route}'`)
98100
}

tests/AuthModule.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,31 @@ describe('AuthModule', () => {
189189
await authModule.apiMiddleware(req, res, () => {})
190190
assert.equal(permissionsChecked, true)
191191
})
192+
193+
it('should treat HEAD as GET when checking unsecured routes', async () => {
194+
// Express runs the GET handler chain for HEAD requests, so HEAD must
195+
// resolve unsecured state against the GET entry rather than crashing on
196+
// an absent `head` bucket in the store.
197+
const authModule = new AuthModule(createMockApp(), { name: 'test-auth' })
198+
authModule.unsecuredRoutes = { get: { '/api/config': true }, post: {}, put: {}, patch: {}, delete: {} }
199+
authModule.isEnabled = false
200+
authModule.permissions = {
201+
check: async () => { assert.fail('permissions.check should not run for a GET-unsecured route') }
202+
}
203+
204+
let nextCalled = false
205+
const req = {
206+
get: () => undefined,
207+
headers: {},
208+
method: 'HEAD',
209+
baseUrl: '/api',
210+
route: { path: '/config/' },
211+
originalUrl: '/api/config'
212+
}
213+
const res = { sendError: () => {} }
214+
215+
await authModule.apiMiddleware(req, res, () => { nextCalled = true })
216+
assert.equal(nextCalled, true)
217+
})
192218
})
193219
})

tests/Permissions.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,21 @@ describe('Permissions', () => {
140140

141141
await assert.doesNotReject(() => permissions.check(req))
142142
})
143+
144+
it('should treat HEAD as GET when resolving scopes', async () => {
145+
// Express runs the GET handler chain for HEAD requests; the `head`
146+
// bucket is absent from the store, so without aliasing this throws.
147+
permissions.secureRoute('/api/head-alias', 'get', ['read:head-alias'])
148+
149+
const req = {
150+
baseUrl: '/api',
151+
path: '/head-alias',
152+
method: 'HEAD',
153+
auth: { isSuper: false, scopes: ['read:head-alias'] }
154+
}
155+
156+
await assert.doesNotReject(() => permissions.check(req))
157+
})
143158
})
144159

145160
describe('constructor', () => {

0 commit comments

Comments
 (0)