90 lines
2.5 KiB
JavaScript
90 lines
2.5 KiB
JavaScript
import statsd from '../lib/statsd.js'
|
|
import { noCacheControl, defaultCacheControl } from './cache-control.js'
|
|
|
|
const STATSD_KEY = 'middleware.handle_invalid_querystrings'
|
|
|
|
// Exported for the sake of end-to-end tests
|
|
export const MAX_UNFAMILIAR_KEYS_BAD_REQUEST = 15
|
|
export const MAX_UNFAMILIAR_KEYS_REDIRECT = 3
|
|
|
|
const RECOGNIZED_KEYS_BY_PREFIX = {
|
|
'/_next/data/': ['versionId', 'productId', 'restPage', 'apiVersion', 'category', 'subcategory'],
|
|
'/api/search': ['query', 'language', 'version', 'page', 'product', 'autocomplete', 'limit'],
|
|
'/api/anchor-redirect': ['hash', 'path'],
|
|
'/api/webhooks': ['category', 'version'],
|
|
'/api/pageinfo': ['pathname'],
|
|
}
|
|
|
|
const RECOGNIZED_KEYS_BY_ANY = new Set([
|
|
// Learning track pages
|
|
'learn',
|
|
'learnProduct',
|
|
// Platform picker
|
|
'platform',
|
|
// Tool picker
|
|
'tool',
|
|
// When apiVersion isn't the only one. E.g. ?apiVersion=XXX&tool=vscode
|
|
'apiVersion',
|
|
// Search
|
|
'query',
|
|
// The drop-downs on "Webhook events and payloads"
|
|
'actionType',
|
|
])
|
|
|
|
export default function handleInvalidQuerystrings(req, res, next) {
|
|
const { method, query, path } = req
|
|
if (method === 'GET' || method === 'HEAD') {
|
|
const originalKeys = Object.keys(query)
|
|
let keys = originalKeys.filter((key) => !RECOGNIZED_KEYS_BY_ANY.has(key))
|
|
if (keys.length > 0) {
|
|
// Before we judge the number of query strings, strip out all the ones
|
|
// we're familiar with.
|
|
for (const [prefix, recognizedKeys] of Object.entries(RECOGNIZED_KEYS_BY_PREFIX)) {
|
|
if (path.startsWith(prefix)) {
|
|
keys = keys.filter((key) => !recognizedKeys.includes(key))
|
|
}
|
|
}
|
|
}
|
|
|
|
if (keys.length >= MAX_UNFAMILIAR_KEYS_BAD_REQUEST) {
|
|
noCacheControl(res)
|
|
|
|
res.status(400).send('Too many unrecognized query string parameters')
|
|
|
|
const tags = [
|
|
'response:400',
|
|
`url:${req.url}`,
|
|
`ip:${req.ip}`,
|
|
`path:${req.path}`,
|
|
`keys:${originalKeys.length}`,
|
|
]
|
|
statsd.increment(STATSD_KEY, 1, tags)
|
|
|
|
return
|
|
}
|
|
|
|
if (keys.length >= MAX_UNFAMILIAR_KEYS_REDIRECT) {
|
|
defaultCacheControl(res)
|
|
const sp = new URLSearchParams(query)
|
|
keys.forEach((key) => sp.delete(key))
|
|
let newURL = req.path
|
|
if (sp.toString()) newURL += `?${sp}`
|
|
|
|
res.redirect(302, newURL)
|
|
|
|
const tags = [
|
|
'response:302',
|
|
`url:${req.url}`,
|
|
`ip:${req.ip}`,
|
|
`path:${req.path}`,
|
|
`keys:${originalKeys.length}`,
|
|
]
|
|
statsd.increment(STATSD_KEY, 1, tags)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
return next()
|
|
}
|