@sap/eslint-plugin-cds
Version:
ESLint plugin including recommended SAP Cloud Application Programming model and environment rules
65 lines (58 loc) • 2.65 kB
JavaScript
const { RULE_CATEGORIES } = require('../../constants')
const { CdsHandlerRule } = require('./CdsHandlerRule')
const isCqlClauseStart = node => ['SELECT', 'UPDATE', 'INSERT', 'DELETE', 'UPSERT'].includes(node.name)
/**
* ESLint goes through member functions in reverse order:
* x.y.z -> z, y, x
* so we can not track when we
* enter a CQL clause, but instead have to check the chain of ancestors for each member
* recursively. If we find the topmost function call is a CQL clause (SELECT, UPDATE, etc.),
* none of the member functions in the chain are allowed to use untagged template strings.
*/
const isInCqlClause = node => {
if (!node) return false
if (node.type === 'CallExpression' && isCqlClauseStart(node.callee)) return true
if (node.type === 'TaggedTemplateExpression' && isCqlClauseStart(node.tag)) return true
// f(...) and f`...` have slightly different structure, the former has .callee, the latter has .tag
// -> use the first that is available to ascend through the call chain
return isInCqlClause(node.callee?.object ?? node.tag?.object)
}
class CqlSelectUseTemplateStrings extends CdsHandlerRule {
CallExpression(node) {
super.CallExpression(node)
const [arg] = node.arguments ?? []
if (arg?.type !== 'TemplateLiteral') return
if (arg.expressions.length === 0) return // no expressions in the template string, so no SQL injection risk
if (!isInCqlClause(node)) return
const [functionName, prefix] = node.callee.type === 'MemberExpression'
// for ….where`...` we need to use the full preceding expression in the following replacement
? [node.callee.property?.name, this.context.getSourceCode().getText(node.callee)]
// for SELECT`...` we can use the function name directly
: [node.callee.name, node.callee.name]
this.context.report({
node,
message: 'Do not use {{functionName}}(`...`) inside CQL statements, which is prone to SQL injections.',
data: { functionName },
suggest: [{
desc: 'Use {{functionName}}`...` instead of {{functionName}}(`...`)',
data: { functionName },
fix: fixer => fixer.replaceText(node, `${prefix}${this.context.getSourceCode().getText(arg)}`)
}]
})
}
}
module.exports = {
meta: {
type: 'problem',
docs: {
recommended: true,
category: RULE_CATEGORIES.javascript,
description: 'Discourage use of SELECT(...), which allows SQL injections, in favour of SELECT`...`.'
},
fixable: 'code',
schema: [],
hasSuggestions: true
},
create: context => new CqlSelectUseTemplateStrings(context).asESLintVisitor()
}