Skip to content

Commit e9336fe

Browse files
authored
Merge pull request #2129 from RasmusWL/python-update-django
Python: update django support
2 parents c90fa1b + fc851b4 commit e9336fe

File tree

35 files changed

+301
-315
lines changed

35 files changed

+301
-315
lines changed

change-notes/1.23/analysis-python.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@
2020
|----------------------------|------------------------|------------|
2121
| Unreachable code | Fewer false positives | Analysis now accounts for uses of `contextlib.suppress` to suppress exceptions. |
2222
| `__iter__` method returns a non-iterator | Better alert message | Alert now highlights which class is expected to be an iterator. |
23+
24+
25+
## Changes to QL libraries
26+
27+
* Django library now recognizes positional arguments from a `django.conf.urls.url` regex (Django version 1.x)

python/ql/src/Security/CWE-089/SqlInjection.qhelp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,29 @@ or prepared statements.
2121

2222
<example>
2323
<p>
24-
In the following snippet, from an example django app,
25-
a name is stored in the database using two different queries.
24+
In the following snippet, a user is fetched from the database using three
25+
different queries.
2626
</p>
2727

2828
<p>
2929
In the first case, the query string is built by
30-
directly using string formatting from a user-supplied request attribute.
30+
directly using string formatting from a user-supplied request parameter.
3131
The parameter may include quote characters, so this
3232
code is vulnerable to a SQL injection attack.
3333
</p>
3434

3535
<p>
3636
In the second case, the user-supplied request attribute is passed
37-
to the database using query parameters.
37+
to the database using query parameters. The database connector library will
38+
take care of escaping and inserting quotes as needed.
39+
</p>
40+
41+
<p>
42+
In the third case, the placeholder in the SQL string has been manually quoted. Since most
43+
databaseconnector libraries will insert their own quotes, doing so yourself will make the code
44+
vulnerable to SQL injection attacks. In this example, if <code>username</code> was
45+
<code>; DROP ALL TABLES -- </code>, the final SQL query would be
46+
<code>SELECT * FROM users WHERE username = ''; DROP ALL TABLES -- ''</code>
3847
</p>
3948

4049
<sample src="examples/sql_injection.py" />
Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
2-
from django.conf.urls import patterns, url
1+
from django.conf.urls import url
32
from django.db import connection
43

54

6-
def save_name(request):
7-
8-
if request.method == 'POST':
9-
name = request.POST.get('name')
10-
curs = connection.cursor()
11-
#BAD -- Using string formatting
12-
curs.execute(
13-
"insert into names_file ('name') values ('%s')" % name)
14-
#GOOD -- Using parameters
15-
curs.execute(
16-
"insert into names_file ('name') values ('%s')", name)
5+
def show_user(request, username):
6+
with connection.cursor() as cursor:
7+
# BAD -- Using string formatting
8+
cursor.execute("SELECT * FROM users WHERE username = '%s'" % username)
9+
user = cursor.fetchone()
1710

11+
# GOOD -- Using parameters
12+
cursor.execute("SELECT * FROM users WHERE username = %s", username)
13+
user = cursor.fetchone()
1814

19-
urlpatterns = patterns(url(r'^save_name/$',
20-
upload, name='save_name'))
15+
# BAD -- Manually quoting placeholder (%s)
16+
cursor.execute("SELECT * FROM users WHERE username = '%s'", username)
17+
user = cursor.fetchone()
2118

19+
urlpatterns = [url(r'^users/(?P<username>[^/]+)$', show_user)]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import python
2+
import semmle.python.regex
3+
import semmle.python.web.Http
4+
5+
predicate django_route(CallNode call, ControlFlowNode regex, FunctionValue view) {
6+
exists(FunctionValue url |
7+
Value::named("django.conf.urls.url") = url and
8+
url.getArgumentForCall(call, 0) = regex and
9+
url.getArgumentForCall(call, 1).pointsTo(view)
10+
)
11+
}
12+
13+
class DjangoRouteRegex extends RegexString {
14+
DjangoRouteRegex() { django_route(_, this.getAFlowNode(), _) }
15+
}
16+
17+
class DjangoRoute extends CallNode {
18+
DjangoRoute() { django_route(this, _, _) }
19+
20+
FunctionValue getViewFunction() { django_route(this, _, result) }
21+
22+
string getNamedArgument() {
23+
exists(DjangoRouteRegex regex |
24+
django_route(this, regex.getAFlowNode(), _) and
25+
regex.getGroupName(_, _) = result
26+
)
27+
}
28+
29+
/**
30+
* Get the number of positional arguments that will be passed to the view.
31+
* Will only return a result if there are no named arguments.
32+
*/
33+
int getNumPositionalArguments() {
34+
exists(DjangoRouteRegex regex |
35+
django_route(this, regex.getAFlowNode(), _) and
36+
not exists(string s | s = regex.getGroupName(_, _)) and
37+
result = count(regex.getGroupNumber(_, _))
38+
)
39+
}
40+
}

python/ql/src/semmle/python/web/django/Model.qll

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -54,33 +54,6 @@ class DjangoModelObjects extends TaintSource {
5454
override string toString() { result = "django.db.models.Model.objects" }
5555
}
5656

57-
/** A write to a field of a django model, which is a vulnerable to external data. */
58-
class DjangoModelFieldWrite extends SqlInjectionSink {
59-
DjangoModelFieldWrite() {
60-
exists(AttrNode attr, DjangoModel model |
61-
this = attr and attr.isStore() and attr.getObject(_).pointsTo(model)
62-
)
63-
}
64-
65-
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
66-
67-
override string toString() { result = "django model field write" }
68-
}
69-
70-
/** A direct reference to a django model object, which is vulnerable to external data. */
71-
class DjangoModelDirectObjectReference extends TaintSink {
72-
DjangoModelDirectObjectReference() {
73-
exists(CallNode objects_get_call, ControlFlowNode objects | this = objects_get_call.getAnArg() |
74-
objects_get_call.getFunction().(AttrNode).getObject("get") = objects and
75-
any(DjangoDbTableObjects objs).taints(objects)
76-
)
77-
}
78-
79-
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
80-
81-
override string toString() { result = "django model object reference" }
82-
}
83-
8457
/**
8558
* A call to the `raw` method on a django model. This allows a raw SQL query
8659
* to be sent to the database, which is a security risk.

python/ql/src/semmle/python/web/django/Request.qll

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import python
2-
import semmle.python.regex
32
import semmle.python.security.TaintTracking
43
import semmle.python.web.Http
4+
import semmle.python.web.django.General
55

66
/** A django.request.HttpRequest object */
77
class DjangoRequest extends TaintKind {
@@ -52,7 +52,7 @@ abstract class DjangoRequestSource extends HttpRequestTaintSource {
5252
private class DjangoFunctionBasedViewRequestArgument extends DjangoRequestSource {
5353
DjangoFunctionBasedViewRequestArgument() {
5454
exists(FunctionValue view |
55-
url_dispatch(_, _, view) and
55+
django_route(_, _, view) and
5656
this = view.getScope().getArg(0).asName().getAFlowNode()
5757
)
5858
}
@@ -67,7 +67,7 @@ private class DjangoView extends ClassValue {
6767
}
6868

6969
private FunctionValue djangoViewHttpMethod() {
70-
exists(DjangoView view | view.attr(httpVerbLower()) = result)
70+
exists(DjangoView view | view.lookup(httpVerbLower()) = result)
7171
}
7272

7373
class DjangoClassBasedViewRequestArgument extends DjangoRequestSource {
@@ -76,41 +76,18 @@ class DjangoClassBasedViewRequestArgument extends DjangoRequestSource {
7676
}
7777
}
7878

79-
/* *********** Routing ********* */
80-
/* Function based views */
81-
predicate url_dispatch(CallNode call, ControlFlowNode regex, FunctionValue view) {
82-
exists(FunctionValue url |
83-
Value::named("django.conf.urls.url") = url and
84-
url.getArgumentForCall(call, 0) = regex and
85-
url.getArgumentForCall(call, 1).pointsTo(view)
86-
)
87-
}
88-
89-
class UrlRegex extends RegexString {
90-
UrlRegex() { url_dispatch(_, this.getAFlowNode(), _) }
91-
}
92-
93-
class UrlRouting extends CallNode {
94-
UrlRouting() { url_dispatch(this, _, _) }
95-
96-
FunctionValue getViewFunction() { url_dispatch(this, _, result) }
97-
98-
string getNamedArgument() {
99-
exists(UrlRegex regex |
100-
url_dispatch(this, regex.getAFlowNode(), _) and
101-
regex.getGroupName(_, _) = result
102-
)
103-
}
104-
}
105-
10679
/** An argument specified in a url routing table */
107-
class HttpRequestParameter extends HttpRequestTaintSource {
108-
HttpRequestParameter() {
109-
exists(UrlRouting url |
110-
this.(ControlFlowNode).getNode() = url
111-
.getViewFunction()
112-
.getScope()
113-
.getArgByName(url.getNamedArgument())
80+
class DjangoRequestParameter extends HttpRequestTaintSource {
81+
DjangoRequestParameter() {
82+
exists(DjangoRoute route, Function f |
83+
f = route.getViewFunction().getScope() |
84+
this.(ControlFlowNode).getNode() = f.getArgByName(route.getNamedArgument())
85+
or
86+
exists(int i | i >= 0 |
87+
i < route.getNumPositionalArguments() and
88+
// +1 because first argument is always the request
89+
this.(ControlFlowNode).getNode() = f.getArg(i+1)
90+
)
11491
)
11592
}
11693

python/ql/test/3/library-tests/web/django/Sinks.expected

Lines changed: 0 additions & 6 deletions
This file was deleted.

python/ql/test/3/library-tests/web/django/Sinks.ql

Lines changed: 0 additions & 13 deletions
This file was deleted.

python/ql/test/3/library-tests/web/django/Sources.expected

Lines changed: 0 additions & 8 deletions
This file was deleted.

python/ql/test/3/library-tests/web/django/Sources.ql

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)