mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[monorepo] prepare for zod everywhere epic (#36027)
GitOrigin-RevId: f082ae7fc7857fa72b2c1823645050bd4f6df305
This commit is contained in:
@@ -16,6 +16,7 @@ module.exports = {
|
||||
'require-rel-for-docs-links': require('./require-rel-for-docs-links'),
|
||||
'require-cio-snake-case-properties': require('./require-cio-snake-case-properties'),
|
||||
'no-throw-in-callback': require('./no-throw-in-callback'),
|
||||
'no-raw-req-access': require('./no-raw-req-access'),
|
||||
'no-consecutive-spaces-in-locales': require('./no-consecutive-spaces-in-locales'),
|
||||
'no-straight-apostrophes-in-locales': require('./no-straight-apostrophes-in-locales'),
|
||||
'french-typography-in-locales': require('./french-typography-in-locales'),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
const RAW_FIELDS = new Set(['body', 'query', 'params'])
|
||||
|
||||
function isReqIdentifier(node) {
|
||||
return node && node.type === 'Identifier' && node.name === 'req'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow reading raw request input (req.body/req.query/req.params); validate it with parseReq() instead',
|
||||
},
|
||||
messages: {
|
||||
noRawReqAccess:
|
||||
'Do not read req.{{field}} directly; validate request input with parseReq() from @overleaf/validation-tools. Allowlisted infrastructure middleware may use getRawReqInput().',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (!isReqIdentifier(node.object)) return
|
||||
const property = node.property
|
||||
const field =
|
||||
!node.computed && property.type === 'Identifier'
|
||||
? property.name
|
||||
: node.computed &&
|
||||
property.type === 'Literal' &&
|
||||
typeof property.value === 'string'
|
||||
? property.value
|
||||
: null
|
||||
if (field === null || !RAW_FIELDS.has(field)) return
|
||||
// writes stay allowed: body parsers and middleware assign req.body
|
||||
const parent = node.parent
|
||||
if (
|
||||
parent.type === 'AssignmentExpression' &&
|
||||
parent.left === node &&
|
||||
parent.operator === '='
|
||||
) {
|
||||
return
|
||||
}
|
||||
context.report({ node, messageId: 'noRawReqAccess', data: { field } })
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (!isReqIdentifier(node.init)) return
|
||||
if (node.id.type !== 'ObjectPattern') return
|
||||
for (const prop of node.id.properties) {
|
||||
if (
|
||||
prop.type === 'Property' &&
|
||||
!prop.computed &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
RAW_FIELDS.has(prop.key.name)
|
||||
) {
|
||||
context.report({
|
||||
node: prop,
|
||||
messageId: 'noRawReqAccess',
|
||||
data: { field: prop.key.name },
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { RuleTester } from 'eslint'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import json from '@eslint/json'
|
||||
import noThrowInCallback from '../../no-throw-in-callback.js'
|
||||
import noRawReqAccess from '../../no-raw-req-access.js'
|
||||
import preferKebabUrl from '../../prefer-kebab-url.js'
|
||||
import noUnnecessaryTrans from '../../no-unnecessary-trans.js'
|
||||
import shouldUnescapeTrans from '../../should-unescape-trans.js'
|
||||
@@ -353,6 +354,65 @@ ruleTester.run('no-throw-in-callback', noThrowInCallback, {
|
||||
],
|
||||
})
|
||||
|
||||
ruleTester.run('no-raw-req-access', noRawReqAccess, {
|
||||
valid: [
|
||||
// validated access goes through parseReq
|
||||
{ code: `const { params, body } = parseReq(req, schema)` },
|
||||
// allowlisted middleware reads raw input explicitly
|
||||
{ code: `const { query } = getRawReqInput(req)` },
|
||||
// writes stay allowed: body parsers and middleware assign req.body
|
||||
{ code: `req.body = {}` },
|
||||
{ code: `req.query = parsed` },
|
||||
// other request fields are not locked
|
||||
{ code: `const ua = req.headers['user-agent']` },
|
||||
{ code: `const s = req.session` },
|
||||
// other objects with the same field names are fine
|
||||
{ code: `const b = options.body` },
|
||||
{ code: `const { body } = response` },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `const id = req.params.project_id`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'params' } }],
|
||||
},
|
||||
{
|
||||
code: `if (req.query.force) { doIt() }`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'query' } }],
|
||||
},
|
||||
{
|
||||
code: `send(req.body)`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
|
||||
},
|
||||
// computed string access
|
||||
{
|
||||
code: `const b = req['body']`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
|
||||
},
|
||||
// destructuring reads
|
||||
{
|
||||
code: `const { body } = req`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
|
||||
},
|
||||
{
|
||||
code: `const { params, query } = req`,
|
||||
errors: [
|
||||
{ messageId: 'noRawReqAccess', data: { field: 'params' } },
|
||||
{ messageId: 'noRawReqAccess', data: { field: 'query' } },
|
||||
],
|
||||
},
|
||||
// compound assignment reads before writing
|
||||
{
|
||||
code: `req.body ||= {}`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
|
||||
},
|
||||
// optional chaining
|
||||
{
|
||||
code: `const q = req?.query`,
|
||||
errors: [{ messageId: 'noRawReqAccess', data: { field: 'query' } }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
ruleTester.run('require-rel-for-docs-links', requireRelForDocsLinks, {
|
||||
valid: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user