|
| 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