Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit e58c93f

Browse files
authored
Merge pull request #10 from webschik/tsr-detect-sql-literal-injection
Added tsr-detect-sql-literal-injection rule
2 parents 8f4ce14 + f00c7c3 commit e58c93f

File tree

8 files changed

+156
-7
lines changed

8 files changed

+156
-7
lines changed

README.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Inspired by [eslint-plugin-security](https://github.com/nodesecurity/eslint-plug
1010

1111
## How to use
1212
* Install package:
13-
```
13+
```shell
1414
npm i tslint-config-security --save-dev --production
1515
```
1616

@@ -22,6 +22,17 @@ npm i tslint-config-security --save-dev --production
2222
}
2323
```
2424

25+
By default `tslint-config-security` enables [all rules](#rules), but you may disable any of them (not recommended):
26+
27+
```json
28+
{
29+
"extends": ["tslint-config-security"],
30+
"rules": {
31+
"tsr-detect-html-injection": false,
32+
"tsr-detect-unsafe-regexp": false
33+
}
34+
```
35+
2536

2637
## Rules
2738
All rules start from the prefix `tsr-` (TSLint Security Rule) to prevent name collisions.
@@ -82,7 +93,7 @@ Due to the known issues in the typed TSLint rules
8293

8394
`tslint-config-security` module will analyze methods only on **fs** variable or on **'fs' module**. E.g.:
8495

85-
```
96+
```js
8697
const fs = require('fs');
8798

8899
fs.open(somePath); // triggers the error
@@ -126,3 +137,16 @@ More information: http://stackoverflow.com/questions/18130254/randombytes-vs-pse
126137
Detects HTML injections:
127138
- `document.write(variable)`
128139
- `Element.innerHTML = variable;`
140+
141+
#### `tsr-detect-sql-literal-injection`
142+
143+
Detects possible SQL injections in string literals:
144+
```js
145+
const userId = 1;
146+
const query1 = `SELECT * FROM users WHERE id = ${userId}`;
147+
const query2 = `SELECT * FROM users WHERE id = ` + userId;
148+
const query2 = 'SELECT * FROM users WHERE id =' + userId;
149+
150+
const columns = 'id, name';
151+
Users.query(`SELECT ${columns} FROM users`);
152+
```

index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
'tsr-detect-pseudo-random-bytes': [true],
1414
'tsr-detect-unsafe-regexp': [true],
1515
'tsr-disable-mustache-escape': [true],
16-
'tsr-detect-html-injection': [true]
16+
'tsr-detect-html-injection': [true],
17+
'tsr-detect-sql-literal-injection': [true]
1718
}
1819
};

npm-shrinkwrap.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tslint-config-security",
3-
"version": "1.10.0",
3+
"version": "1.11.0",
44
"description": "TSLint security rules",
55
"main": "./index.js",
66
"files": [
@@ -42,7 +42,8 @@
4242
"typescript": "^3.0.1"
4343
},
4444
"peerDependencies": {
45-
"tslint": "^5.8.0"
45+
"tslint": "^5.8.0",
46+
"tslib": "^1.9.2"
4647
},
4748
"dependencies": {
4849
"safe-regex": "^1.1.0"

src/is-sql-query.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
function createMainKeywordsPattern(keyword: string): RegExp {
2+
return new RegExp(`(^|\\s)(${keyword})`);
3+
}
4+
5+
const selectKeyword: RegExp = createMainKeywordsPattern('SELECT');
6+
const deleteKeyword: RegExp = createMainKeywordsPattern('DELETE');
7+
const insertKeyword: RegExp = createMainKeywordsPattern('INSERT');
8+
const updateKeyword: RegExp = createMainKeywordsPattern('UPDATE');
9+
const dropKeyword: RegExp = createMainKeywordsPattern('DROP');
10+
const createKeyword: RegExp = createMainKeywordsPattern('CREATE');
11+
const alterKeyword: RegExp = createMainKeywordsPattern('ALTER');
12+
13+
/**
14+
* @description Basic SQL query detection
15+
* @param {string} q
16+
* @returns {boolean}
17+
*/
18+
export function isSqlQuery(q: string): boolean {
19+
// detect the shortest sql query
20+
if (!q[11]) {
21+
return false;
22+
}
23+
24+
const query: string = q.toUpperCase();
25+
26+
if (selectKeyword.test(query) && (query.includes(' FROM ') || query.includes('*FROM '))) {
27+
return true;
28+
}
29+
30+
if (insertKeyword.test(query) && query.includes(' INTO ')) {
31+
return true;
32+
}
33+
34+
if (updateKeyword.test(query) && query.includes(' SET ')) {
35+
return true;
36+
}
37+
38+
if (deleteKeyword.test(query) && query.includes(' FROM ')) {
39+
return true;
40+
}
41+
42+
if (dropKeyword.test(query) && (query.includes(' TABLE ') || query.includes(' DATABASE '))) {
43+
return true;
44+
}
45+
46+
if (
47+
createKeyword.test(query) &&
48+
(query.includes(' INDEX ') || query.includes(' TABLE ') || query.includes(' DATABASE '))
49+
) {
50+
return true;
51+
}
52+
53+
if (alterKeyword.test(query) && query.includes(' TABLE ')) {
54+
return true;
55+
}
56+
57+
return false;
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as ts from 'typescript';
2+
import * as Lint from 'tslint';
3+
import {isSqlQuery} from '../is-sql-query';
4+
5+
export class Rule extends Lint.Rules.AbstractRule {
6+
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
7+
return this.applyWithWalker(new RuleWalker(sourceFile, this.getOptions()));
8+
}
9+
}
10+
11+
const stringLiteralKinds: number[] = [ts.SyntaxKind.NoSubstitutionTemplateLiteral, ts.SyntaxKind.StringLiteral];
12+
const generalErrorMessage: string = 'Found possible SQL injection';
13+
14+
class RuleWalker extends Lint.RuleWalker {
15+
visitTemplateExpression(node: ts.TemplateExpression) {
16+
const {parent} = node;
17+
18+
if (
19+
(!parent || parent.kind !== ts.SyntaxKind.TaggedTemplateExpression) &&
20+
isSqlQuery(node.getText().slice(1, -1))
21+
) {
22+
this.addFailureAtNode(node, generalErrorMessage);
23+
}
24+
25+
super.visitTemplateExpression(node);
26+
}
27+
28+
visitBinaryExpression(node: ts.BinaryExpression) {
29+
const {left} = node;
30+
31+
if (left && stringLiteralKinds.includes(left.kind) && isSqlQuery(left.getText().slice(1, -1))) {
32+
this.addFailureAtNode(left, generalErrorMessage);
33+
}
34+
35+
super.visitBinaryExpression(node);
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const userId = 1;
2+
let query = `SELECT * FROM users WHERE id = ${userId}`;
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found possible SQL injection]
4+
query = `SELECT *FROM users WHERE id = ` + userId;
5+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found possible SQL injection]
6+
query = ' SELECT * FROM users WHERE id =' + userId;
7+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found possible SQL injection]
8+
9+
db.query(query);
10+
11+
const columns = 'id, name';
12+
Users.query( ` SELECT ${columns} FROM users` );
13+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Found possible SQL injection]
14+
15+
16+
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
17+
db.query(query);
18+
19+
Users.query(`SELECT id, name FROM users`);
20+
21+
const punctuation = '!';
22+
console.log(`Not SQL${punctuation}`);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"rulesDirectory": "../../../../dist/rules",
3+
"rules": {
4+
"tsr-detect-sql-literal-injection": true
5+
}
6+
}

0 commit comments

Comments
 (0)