Skip to content

Commit b78cd48

Browse files
authored
Merge pull request github#13329 from erik-krogh/sqlhelp
JS: improve the sql-injection help page
2 parents 29bbf58 + 3cb2ec4 commit b78cd48

File tree

11 files changed

+249
-158
lines changed

11 files changed

+249
-158
lines changed

javascript/ql/src/Security/CWE-089/SqlInjection.inc.qhelp

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ or prepared statements.
2020
<p>
2121
For NoSQL queries, make use of an operator like MongoDB's <code>$eq</code>
2222
to ensure that untrusted data is interpreted as a literal value and not as
23-
a query object.
23+
a query object. Alternatively, check that the untrusted data is a literal
24+
value and not a query object before using it in a query.
25+
</p>
26+
<p>
27+
For SQL queries, use query parameters or prepared statements to
28+
embed untrusted data into the query string, or use a library like
29+
<code>sqlstring</code> to escape untrusted data.
2430
</p>
2531
</recommendation>
2632

@@ -32,31 +38,61 @@ an HTTP request handler in a web application, whose parameter
3238
</p>
3339

3440
<p>
35-
The handler constructs two copies of the same SQL query involving
36-
user input taken from the request object, once unsafely using
37-
string concatenation, and once safely using query parameters.
41+
The handler constructs an SQL query string from user input
42+
and executes it as a database query using the <code>pg</code> library.
43+
The user input may contain quote characters, so this code is vulnerable
44+
to a SQL injection attack.
3845
</p>
3946

40-
<p>
41-
In the first case, the query string <code>query1</code> is built by
42-
directly concatenating a user-supplied request parameter with some
43-
string literals. The parameter may include quote characters, so this
44-
code is vulnerable to a SQL injection attack.
45-
</p>
47+
<sample src="examples/SqlInjection.js" />
4648

4749
<p>
48-
In the second case, the parameter is embedded into the query string
49-
<code>query2</code> using query parameters. In this example, we use
50+
To fix this vulnerability, we can use query parameters to embed the
51+
user input into the query string. In this example, we use
5052
the API offered by the <code>pg</code> Postgres database connector
5153
library, but other libraries offer similar features. This version is
5254
immune to injection attacks.
5355
</p>
5456

55-
<sample src="examples/SqlInjection.js" />
57+
<sample src="examples/SqlInjectionFix.js" />
58+
59+
<p>
60+
Alternatively, we can use a library like <code>sqlstring</code> to
61+
escape the user input before embedding it into the query string:
62+
</p>
63+
<sample src="examples/SqlInjectionFix2.js" />
64+
</example>
65+
66+
<example>
67+
<p>
68+
In the following example, an express handler attempts to delete
69+
a single document from a MongoDB collection. The document to be
70+
deleted is identified by its <code>_id</code> field, which is
71+
constructed from user input. The user input may contain a query
72+
object, so this code is vulnerable to a NoSQL injection attack.
73+
</p>
74+
75+
<sample src="examples/NoSqlInjection.js" />
76+
77+
<p>
78+
To fix this vulnerability, we can use the <code>$eq</code> operator
79+
to ensure that the user input is interpreted as a literal value
80+
and not as a query object:
81+
</p>
82+
83+
<sample src="examples/NoSqlInjectionFix.js" />
84+
85+
<p>
86+
Alternatively check that the user input is a
87+
literal value and not a query object before using it:
88+
</p>
89+
90+
<sample src="examples/NoSqlInjectionFix2.js" />
5691
</example>
5792

5893
<references>
5994
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL injection</a>.</li>
6095
<li>MongoDB: <a href="https://docs.mongodb.com/manual/reference/operator/query/eq">$eq operator</a>.</li>
96+
<li>OWASP: <a href="https://owasp.org/www-pdf-archive/GOD16-NOSQL.pdf">NoSQL injection</a>.</li>
6197
</references>
6298
</qhelp>

javascript/ql/src/Security/CWE-089/SqlInjection.ql

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import semmle.javascript.security.dataflow.SqlInjectionQuery as SqlInjection
1818
import semmle.javascript.security.dataflow.NosqlInjectionQuery as NosqlInjection
1919
import DataFlow::PathGraph
2020

21-
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
21+
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, string type
2222
where
2323
(
24-
cfg instanceof SqlInjection::Configuration or
25-
cfg instanceof NosqlInjection::Configuration
24+
cfg instanceof SqlInjection::Configuration and type = "string"
25+
or
26+
cfg instanceof NosqlInjection::Configuration and type = "object"
2627
) and
2728
cfg.hasFlowPath(source, sink)
28-
select sink.getNode(), source, sink, "This query depends on a $@.", source.getNode(),
29+
select sink.getNode(), source, sink, "This query " + type + " depends on a $@.", source.getNode(),
2930
"user-provided value"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const express = require("express");
2+
const mongoose = require("mongoose");
3+
const Todo = mongoose.model(
4+
"Todo",
5+
new mongoose.Schema({ text: { type: String } }, { timestamps: true })
6+
);
7+
8+
const app = express();
9+
app.use(express.json());
10+
app.use(express.urlencoded({ extended: false }));
11+
12+
app.delete("/api/delete", async (req, res) => {
13+
let id = req.body.id;
14+
15+
await Todo.deleteOne({ _id: id }); // BAD: id might be an object with special properties
16+
17+
res.json({ status: "ok" });
18+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
app.delete("/api/delete", async (req, res) => {
2+
let id = req.body.id;
3+
await Todo.deleteOne({ _id: { $eq: id } }); // GOOD: using $eq operator for the comparison
4+
5+
res.json({ status: "ok" });
6+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
app.delete("/api/delete", async (req, res) => {
2+
let id = req.body.id;
3+
if (typeof id !== "string") {
4+
res.status(400).json({ status: "error" });
5+
return;
6+
}
7+
await Todo.deleteOne({ _id: id }); // GOOD: id is guaranteed to be a string
8+
9+
res.json({ status: "ok" });
10+
});

javascript/ql/src/Security/CWE-089/examples/SqlInjection.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,4 @@ app.get("search", function handler(req, res) {
1111
pool.query(query1, [], function(err, results) {
1212
// process results
1313
});
14-
15-
// GOOD: use parameters
16-
var query2 =
17-
"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=$1" + " ORDER BY PRICE";
18-
pool.query(query2, [req.params.category], function(err, results) {
19-
// process results
20-
});
2114
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const app = require("express")(),
2+
pg = require("pg"),
3+
pool = new pg.Pool(config);
4+
5+
app.get("search", function handler(req, res) {
6+
// GOOD: use parameters
7+
var query2 =
8+
"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=$1 ORDER BY PRICE";
9+
pool.query(query2, [req.params.category], function(err, results) {
10+
// process results
11+
});
12+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const app = require("express")(),
2+
pg = require("pg"),
3+
SqlString = require('sqlstring'),
4+
pool = new pg.Pool(config);
5+
6+
app.get("search", function handler(req, res) {
7+
// GOOD: the category is escaped using mysql.escape
8+
var query1 =
9+
"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='" +
10+
SqlString.escape(req.params.category) +
11+
"' ORDER BY PRICE";
12+
pool.query(query1, [], function(err, results) {
13+
// process results
14+
});
15+
});

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-089/SqlInjection.ql

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import semmle.javascript.security.dataflow.NosqlInjectionQuery as NosqlInjection
2020
import DataFlow::PathGraph
2121
import semmle.javascript.heuristics.AdditionalSources
2222

23-
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
23+
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, string type
2424
where
2525
(
26-
cfg instanceof SqlInjection::Configuration or
27-
cfg instanceof NosqlInjection::Configuration
26+
cfg instanceof SqlInjection::Configuration and type = "string"
27+
or
28+
cfg instanceof NosqlInjection::Configuration and type = "object"
2829
) and
29-
cfg.hasFlowPath(source, sink) and
30-
source.getNode() instanceof HeuristicSource
31-
select sink.getNode(), source, sink, "This query depends on a $@.", source.getNode(),
30+
cfg.hasFlowPath(source, sink)
31+
select sink.getNode(), source, sink, "This query " + type + " depends on a $@.", source.getNode(),
3232
"user-provided value"

javascript/ql/test/query-tests/Security/CWE-089/typed/SqlInjection.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ edges
3737
| typedClient.ts:23:33:23:33 | v | typedClient.ts:23:27:23:35 | { id: v } |
3838
| typedClient.ts:23:33:23:33 | v | typedClient.ts:23:27:23:35 | { id: v } |
3939
#select
40-
| typedClient.ts:14:24:14:32 | { id: v } | typedClient.ts:13:22:13:29 | req.body | typedClient.ts:14:24:14:32 | { id: v } | This query depends on a $@. | typedClient.ts:13:22:13:29 | req.body | user-provided value |
41-
| typedClient.ts:22:27:22:35 | { id: v } | typedClient.ts:21:22:21:29 | req.body | typedClient.ts:22:27:22:35 | { id: v } | This query depends on a $@. | typedClient.ts:21:22:21:29 | req.body | user-provided value |
42-
| typedClient.ts:23:27:23:35 | { id: v } | typedClient.ts:21:22:21:29 | req.body | typedClient.ts:23:27:23:35 | { id: v } | This query depends on a $@. | typedClient.ts:21:22:21:29 | req.body | user-provided value |
40+
| typedClient.ts:14:24:14:32 | { id: v } | typedClient.ts:13:22:13:29 | req.body | typedClient.ts:14:24:14:32 | { id: v } | This query object depends on a $@. | typedClient.ts:13:22:13:29 | req.body | user-provided value |
41+
| typedClient.ts:22:27:22:35 | { id: v } | typedClient.ts:21:22:21:29 | req.body | typedClient.ts:22:27:22:35 | { id: v } | This query object depends on a $@. | typedClient.ts:21:22:21:29 | req.body | user-provided value |
42+
| typedClient.ts:23:27:23:35 | { id: v } | typedClient.ts:21:22:21:29 | req.body | typedClient.ts:23:27:23:35 | { id: v } | This query object depends on a $@. | typedClient.ts:21:22:21:29 | req.body | user-provided value |

0 commit comments

Comments
 (0)