Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ You can use it as is without passing any option or you can configure it as expla
* `preflight`: Disables preflight by passing `false`. Default: `true`.
* `strictPreflight`: Enforces strict requirements for the CORS preflight request headers (**Access-Control-Request-Method** and **Origin**) as defined by the [W3C CORS specification](https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests). Preflight requests without the required headers result in 400 errors when set to `true`. Default: `true`.
* `hideOptionsRoute`: Hides the options route from documentation built using [@fastify/swagger](https://github.com/fastify/fastify-swagger). Default: `true`.
* `logLevel`: Sets the Fastify log level **only** for the internal CORS pre-flight `OPTIONS *` route.
Pass `'silent'` to suppress these requests in your logs, or any valid Fastify
log level (`'trace'`, `'debug'`, `'info'`, `'warn'`, `'error'`, `'fatal'`).
Default: inherits Fastify’s global log level.

#### :warning: DoS attacks

Expand Down
8 changes: 6 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ function fastifyCors (fastify, opts, next) {
fastify.decorateRequest('corsPreflightEnabled', false)

let hideOptionsRoute = true
let logLevel

if (typeof opts === 'function') {
handleCorsOptionsDelegator(opts, fastify, { hook: defaultOptions.hook }, next)
} else if (opts.delegator) {
const { delegator, ...options } = opts
handleCorsOptionsDelegator(delegator, fastify, options, next)
} else {
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
const corsOptions = normalizeCorsOptions(opts)
validateHook(corsOptions.hook, next)
if (hookWithPayload.indexOf(corsOptions.hook) !== -1) {
Expand All @@ -65,14 +66,17 @@ function fastifyCors (fastify, opts, next) {
})
}
}
if (opts.logLevel !== undefined) logLevel = opts.logLevel
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute

// The preflight reply must occur in the hook. This allows fastify-cors to reply to
// preflight requests BEFORE possible authentication plugins. If the preflight reply
// occurred in this handler, other plugins may deny the request since the browser will
// remove most headers (such as the Authentication header).
//
// This route simply enables fastify to accept preflight requests.
fastify.options('*', { schema: { hide: hideOptionsRoute } }, (req, reply) => {

fastify.options('*', { schema: { hide: hideOptionsRoute }, logLevel }, (req, reply) => {
if (!req.corsPreflightEnabled) {
// Do not handle preflight requests if the origin option disabled CORS
reply.callNotFound()
Expand Down
102 changes: 102 additions & 0 deletions test/preflight.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,105 @@ test('Should support ongoing prefix ', async t => {
'content-length': '0'
})
})

test('Silences preflight logs when logLevel is "silent"', async t => {
const logs = []
const fastify = Fastify({
logger: {
level: 'info',
stream: {
write (line) {
try {
logs.push(JSON.parse(line))
} catch {
}
}
}
}
})

await fastify.register(cors, { logLevel: 'silent' })

fastify.get('/', async () => ({ ok: true }))

await fastify.ready()
t.assert.ok(fastify)

await fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'https://example.com'
}
})

await fastify.inject({ method: 'GET', url: '/' })

const hasOptionsLog = logs.some(l => l.req && l.req.method === 'OPTIONS')
const hasGetLog = logs.some(l => l.req && l.req.method === 'GET')

t.assert.strictEqual(hasOptionsLog, false)
t.assert.strictEqual(hasGetLog, true)

await fastify.close()
})
test('delegator + logLevel:"silent" → OPTIONS logs are suppressed', async t => {
t.plan(3)

const logs = []
const app = Fastify({
logger: {
level: 'info',
stream: { write: l => { try { logs.push(JSON.parse(l)) } catch {} } }
}
})

await app.register(cors, {
delegator: () => ({ origin: '*' }),
logLevel: 'silent'
})

app.get('/', () => ({ ok: true }))
await app.ready()
t.assert.ok(app)

await app.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'https://example.com'
}
})

await app.inject({ method: 'GET', url: '/' })

const hasOptionsLog = logs.some(l => l.req?.method === 'OPTIONS')
const hasGetLog = logs.some(l => l.req?.method === 'GET')

t.assert.strictEqual(hasOptionsLog, false)
t.assert.strictEqual(hasGetLog, true)

await app.close()
})
test('delegator + hideOptionsRoute:false → OPTIONS route is visible', async t => {
t.plan(2)

const app = Fastify()

app.addHook('onRoute', route => {
if (route.method === 'OPTIONS' && route.url === '*') {
t.assert.strictEqual(route.schema.hide, false)
}
})

await app.register(cors, {
delegator: () => ({ origin: '*' }),
hideOptionsRoute: false
})

await app.ready()
t.assert.ok(app)
await app.close()
})
11 changes: 10 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="node" />

import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from 'fastify'
import { FastifyInstance, FastifyPluginCallback, FastifyRequest, LogLevel } from 'fastify'

type OriginCallback = (err: Error | null, origin: ValueOrArray<OriginType>) => void
type OriginType = string | boolean | RegExp
Expand Down Expand Up @@ -99,6 +99,15 @@ declare namespace fastifyCors {
* Hide options route from the documentation built using fastify-swagger (default: true).
*/
hideOptionsRoute?: boolean;

/**
* Sets the Fastify log level specifically for the internal OPTIONS route
* used to handle CORS preflight requests. For example, setting this to `'silent'`
* will prevent these requests from being logged.
* Useful for reducing noise in application logs.
* Default: inherits Fastify's global log level.
*/
logLevel?: LogLevel;
}

export interface FastifyCorsOptionsDelegateCallback { (req: FastifyRequest, cb: (error: Error | null, corsOptions?: FastifyCorsOptions) => void): void }
Expand Down
3 changes: 2 additions & 1 deletion types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ appHttp2.register(fastifyCors, {
preflightContinue: false,
optionsSuccessStatus: 200,
preflight: false,
strictPreflight: false
strictPreflight: false,
logLevel: 'silent'
})

appHttp2.register(fastifyCors, {
Expand Down