Skip to content

Commit c78fed6

Browse files
committed
Python: ORM: Add raw python test files
no ql test files yet though, will come in next commit.
1 parent f89fb50 commit c78fed6

File tree

13 files changed

+630
-4
lines changed

13 files changed

+630
-4
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
db.sqlite3
2+
3+
# The testapp/migrations/ folder needs to be comitted to git,
4+
# but we don't care to store the actual migrations
5+
testapp/migrations/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
The main test files are:
2+
3+
- [testapp/orm_tests.py](testapp/orm_tests.py): which tests flow from source to sink
4+
- [testapp/orm_security_tests.py](testapp/orm_form_test.py): shows how forms can be used to save Models to the DB
5+
- [testapp/orm_security_tests.py](testapp/orm_security_tests.py): which highlights some interesting interactions with security queries
6+
7+
## Setup
8+
9+
```
10+
pip install django pytest pytest-django
11+
```
12+
13+
## Run server
14+
15+
```
16+
python manage.py makemigrations && python manage.py migrate && python manage.py runserver
17+
```
18+
19+
## Run tests
20+
21+
```
22+
pytest
23+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# to force extractor to see files. since we use `--max-import-depth=1`, we use this
2+
# "fake" import that doesn't actually work, but tricks the python extractor to look at
3+
# all the files
4+
5+
from testapp import *
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[pytest]
2+
DJANGO_SETTINGS_MODULE = testproj.settings
3+
python_files = tests.py
4+
# don't require that you have manually run `python manage.py makemigrations`
5+
addopts = --no-migrations --ignore-glob=*.testproj/ -v
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
from django.db import models
22

33
# Create your models here.
4+
from .orm_tests import *
5+
from .orm_security_tests import *
6+
from .orm_form_test import *
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from django.db import models
2+
from django.http.response import HttpResponse
3+
from django.shortcuts import render
4+
from django import forms
5+
6+
class MyModel(models.Model):
7+
text = models.CharField(max_length=256)
8+
9+
class MyModelForm(forms.ModelForm):
10+
# see https://docs.djangoproject.com/en/4.0/topics/forms/modelforms/#django.forms.ModelForm
11+
class Meta:
12+
model = MyModel
13+
fields = ["text"]
14+
15+
# TODO: When we actually start supporting ModelForm, we need to add test-cases for
16+
# limiting what fields are used. See
17+
# https://docs.djangoproject.com/en/4.0/topics/forms/modelforms/#selecting-the-fields-to-use
18+
19+
def add_mymodel_handler(request):
20+
if request.method == "POST":
21+
form = MyModelForm(request.POST, request.FILES)
22+
if form.is_valid():
23+
new_MyMoodel_instance = form.save()
24+
return HttpResponse("ok")
25+
else:
26+
print("not valid", form.errors)
27+
else:
28+
form = MyModelForm(initial=request.GET)
29+
30+
return render(request, "form_example.html", {"form": form})
31+
32+
33+
def show_mymodel_handler(request):
34+
obj = MyModel.objects.last()
35+
return HttpResponse("Last object (id={}) had text: {!r}".format(obj.id, obj.text))
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
Handling of ORM steps that are only relevant to real taint-tracking queries, and not core dataflow.
3+
"""
4+
5+
from django.db import models
6+
from django.http.response import HttpResponse
7+
8+
# ------------------------------------------------------------------------------
9+
# Some fields are not relevant for some security queries
10+
# ------------------------------------------------------------------------------
11+
12+
# TODO: We need some way to mark that a certain data-flow node can only contain
13+
# an integer, so it can be excluded from queries.
14+
15+
class Person(models.Model):
16+
name = models.CharField(max_length=256)
17+
age = models.IntegerField()
18+
19+
def person(request):
20+
if request.method == "POST":
21+
person = Person()
22+
person.name = request.POST["name"]
23+
person.age = request.POST["age"]
24+
25+
# at this point, `person.age` is a string, and could contain anything
26+
assert isinstance(person.age, str)
27+
28+
person.save()
29+
30+
# after saving, there will be an error if the string could not be converted to an integer.
31+
# the attribute on the local object is not changed (so still `str`) but after fetching from DB it is
32+
# an `int`
33+
assert isinstance(person.age, str)
34+
35+
# after doing `.full_clean` it also has the proper data-type
36+
person.full_clean()
37+
assert isinstance(person.age, int)
38+
39+
return HttpResponse("ok")
40+
elif request.method == "GET":
41+
resp_text = "<h1>Persons:</h1>"
42+
for person in Person.objects.all():
43+
resp_text += "\n{} (age {})".format(person.name, person.age)
44+
return HttpResponse(resp_text) # NOT OK
45+
46+
def show_name(request):
47+
person = Person.objects.get(id=request.GET["id"])
48+
return HttpResponse("Name is: {}".format(person.name)) # NOT OK
49+
50+
def show_age(request):
51+
person = Person.objects.get(id=request.GET["id"])
52+
assert isinstance(person.age, int)
53+
54+
# Since the age is an integer, there is not actually XSS in the line below
55+
return HttpResponse("Age is: {}".format(person.age)) # OK
56+
57+
# look at the log after doing
58+
"""
59+
http -f 'http://127.0.0.1:8000/person/' name="foo" age=42
60+
http 'http://127.0.0.1:8000/show_age/?id=1'
61+
"""
62+
63+
# ------------------------------------------------------------------------------
64+
# Custom validators on fields
65+
# ------------------------------------------------------------------------------
66+
#
67+
# We currently do not include these in any of our queries. There are two reasons:
68+
#
69+
# 1. We don't have any good way to determine what the validator actually does. So we
70+
# don't have any way to determine if a validator would make data safe for a
71+
# particular query. So we would have to blindly trust that if any validator what
72+
# specified, that would mean data is always safe :| We still want to produce more
73+
# results, so by default, we would want to do it this way, and live live with the FPs
74+
# that arise -- if it turns out that is too troublesome, we can look more into it.
75+
#
76+
# 2. Using a validator on the input data does not make any guarantees on the data that
77+
# is already in the DB. It's better to perform escaping on the data as part of
78+
# outputing/rendering, since you know _all_ data will be escaped, and that the right
79+
# kind of escaping is applied (there is a difference in what needs to be escaped for
80+
# different vulnerabilities, so doing validation that rejects things that would cause
81+
# XSS might still accept things that can do SQL injection)
82+
83+
84+
from django.core.exceptions import ValidationError
85+
import re
86+
87+
def only_az(value):
88+
if not re.match(r"^[a-zA-Z]$", value):
89+
raise ValidationError("only a-zA-Z allowed")
90+
91+
# First example: Validator is set, but not used
92+
class CommentValidatorNotUsed(models.Model):
93+
text = models.CharField(max_length=256, validators=[only_az])
94+
95+
def save_comment_validator_not_used(request): # $requestHandler
96+
comment = CommentValidatorNotUsed(text=request.POST["text"])
97+
comment.save()
98+
return HttpResponse("ok")
99+
100+
def display_comment_validator_not_used(request): # $requestHandler
101+
comment = CommentValidatorNotUsed.objects.last()
102+
return HttpResponse(comment.text) # NOT OK
103+
104+
# To test this
105+
"""
106+
http -f http://127.0.0.1:8000/save_comment_validator_not_used/ text="foo!@#"
107+
http http://127.0.0.1:8000/display_comment_validator_not_used/
108+
"""
109+
110+
# Second example: Validator is set, AND is used
111+
class CommentValidatorUsed(models.Model):
112+
text = models.CharField(max_length=256, validators=[only_az])
113+
114+
def save_comment_validator_used(request): # $requestHandler
115+
comment = CommentValidatorUsed(text=request.POST["text"])
116+
comment.full_clean()
117+
comment.save()
118+
119+
def display_comment_validator_used(request): # $requestHandler
120+
comment = CommentValidatorUsed.objects.last()
121+
return HttpResponse(comment.text) # sort of OK
122+
123+
# Doing the following will raise a ValidationError
124+
"""
125+
http -f http://127.0.0.1:8000/save_comment_validator_used/ text="foo!@#"
126+
"""

0 commit comments

Comments
 (0)