Skip to content

Commit 71fb6e5

Browse files
authored
Merge pull request #75 from davidoc/71_django_framework_adaptor
Add Django framework adaptor
2 parents a762e00 + 738a0f8 commit 71fb6e5

11 files changed

+149
-15
lines changed

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,6 @@ target/
6565
.ipynb_checkpoints
6666
*~
6767
*#
68+
69+
#IDE
70+
.idea/

Diff for: example/example_inputs/django_views.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
def django_view_function(request, x):
2+
return x
3+
4+
5+
class DjangoViewClass(object):
6+
def __init__(self):
7+
pass
8+
9+
@classmethod
10+
def as_view(cls):
11+
def view_function(request, x):
12+
return x
13+
return view_function
14+
15+
16+
# in practice, this would be called in a Django URLconf file
17+
view = DjangoViewClass.as_view()

Diff for: example/example_inputs/flask_function_and_normal_functions.py

+3
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ def _hidden_foo():
88
def flask_function(x):
99
return x
1010

11+
def django_function(request, x):
12+
return x
13+
1114
print('nothing')

Diff for: example/vulnerable_code/django_XSS.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.shortcuts import render
2+
3+
4+
def xss1(request, param):
5+
return render(request, 'templates/xss.html', {'param': param})
6+

Diff for: pyt/__main__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from .framework_helper import (
1616
is_flask_route_function,
1717
is_function,
18-
is_function_without_leading_
19-
)
18+
is_function_without_leading_,
19+
is_django_view_function)
2020
from .github_search import scan_github, set_github_api_token
2121
from .interprocedural_cfg import interprocedural
2222
from .intraprocedural_cfg import intraprocedural
@@ -83,7 +83,7 @@ def parse_args(args):
8383
help='Choose logging level: CRITICAL, ERROR,' +
8484
' WARNING(Default), INFO, DEBUG, NOTSET.', type=str)
8585
parser.add_argument('-a', '--adaptor',
86-
help='Choose an adaptor: Flask(Default) or Every or Pylons.',
86+
help='Choose an adaptor: Flask(Default) or Every or Pylons or Django.',
8787
type=str)
8888
parser.add_argument('-db', '--create-database',
8989
help='Creates a sql file that can be used to' +
@@ -226,6 +226,8 @@ def main(command_line_args=sys.argv[1:]):
226226
framework_route_criteria = is_function
227227
elif args.adaptor and args.adaptor.lower().startswith('p'):
228228
framework_route_criteria = is_function_without_leading_
229+
elif args.adaptor and args.adaptor.lower().startswith('d'):
230+
framework_route_criteria = is_django_view_function
229231
else:
230232
framework_route_criteria = is_flask_route_function
231233
# Add all the route functions to the cfg_list

Diff for: pyt/framework_adaptor.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ def get_func_cfg_with_tainted_args(self, definition):
4747
function_entry_node.connect(tainted_node)
4848
# 1 and not 0 so that Entry Node remains first in the list
4949
func_cfg.nodes.insert(1, tainted_node)
50-
51-
first_arg = func_cfg.nodes[len(args)]
52-
first_arg.connect(first_node_after_args)
50+
tainted_node.connect(first_node_after_args)
5351

5452
return func_cfg
5553

Diff for: pyt/framework_helper.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Provides helper functions that help with determining if a function is a route function."""
22
import ast
33

4-
from .ast_helper import get_call_names
4+
from pyt.base_cfg import Function
5+
from .ast_helper import get_call_names, Arguments
56

67

78
def is_function(function):
@@ -18,6 +19,13 @@ def is_flask_route_function(ast_node):
1819
return False
1920

2021

22+
def is_django_view_function(ast_node):
23+
if len(ast_node.args.args):
24+
first_arg_name = ast_node.args.args[0].arg
25+
return first_arg_name == 'request'
26+
return False
27+
28+
2129
def is_function_without_leading_(ast_node):
2230
if ast_node.name.startswith('_'):
2331
return False

Diff for: pyt/trigger_definitions/django_trigger_words.pyt

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
sources:
2+
POST.get(
3+
GET.get(
4+
META.get(
5+
POST[
6+
GET[
7+
META[
8+
FILES[
9+
.data
10+
form[
11+
form(
12+
mark_safe(
13+
cookies[
14+
files[
15+
SQLAlchemy
16+
17+
sinks:
18+
replace( -> escape
19+
send_file( -> '..', '..' in
20+
execute(
21+
system(
22+
filter(
23+
subprocess.call(
24+
render_template(
25+
set_cookie(
26+
redirect(
27+
url_for(
28+
flash(
29+
jsonify(
30+
render(
31+
render_to_response(
32+
Popen(

Diff for: pyt/vulnerabilities.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def identify_triggers(cfg, sources, sinks, lattice):
6868
"""
6969
assignment_nodes = filter_cfg_nodes(cfg, AssignmentNode)
7070
tainted_nodes = filter_cfg_nodes(cfg, TaintedNode)
71-
tainted_trigger_nodes = [TriggerNode('Flask function URL parameter', None,
71+
tainted_trigger_nodes = [TriggerNode('Framework function URL parameter', None,
7272
node) for node in tainted_nodes]
7373
sources_in_file = find_triggers(assignment_nodes, sources)
7474
sources_in_file.extend(tainted_trigger_nodes)

Diff for: tests/framework_helper_test.py

+34-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from pyt.framework_helper import (
44
is_flask_route_function,
55
is_function,
6-
is_function_without_leading_
6+
is_function_without_leading_,
7+
is_django_view_function
78
)
89

910
class FrameworkEngineTest(BaseTestCase):
@@ -32,8 +33,8 @@ def test_find_every_function_without_leading_underscore(self):
3233
for func in funcs:
3334
if is_function_without_leading_(func.node):
3435
i = i + 1
35-
# So it is supposed to be 2, because we count all functions without a leading underscore
36-
self.assertEqual(i, 2)
36+
# So it is supposed to be 3, because we count all functions without a leading underscore
37+
self.assertEqual(i, 3)
3738

3839
def test_find_every_function(self):
3940
self.cfg_create_from_file('example/example_inputs/flask_function_and_normal_functions.py')
@@ -45,5 +46,33 @@ def test_find_every_function(self):
4546
for func in funcs:
4647
if is_function(func.node):
4748
i = i + 1
48-
# So it is supposed to be 3, because we count all functions
49-
self.assertEqual(len(funcs), 3)
49+
# So it is supposed to be 4, because we count all functions
50+
self.assertEqual(len(funcs), 4)
51+
52+
def test_find_django_functions(self):
53+
self.cfg_create_from_file('example/example_inputs/flask_function_and_normal_functions.py')
54+
55+
cfg_list = [self.cfg]
56+
funcs = _get_func_nodes()
57+
58+
i = 0
59+
for func in funcs:
60+
if is_django_view_function(func.node):
61+
self.assertEqual(func.node.name, 'django_function')
62+
i = i + 1
63+
# So it is supposed to be 1
64+
self.assertEqual(i, 1)
65+
66+
def test_find_django_views(self):
67+
self.cfg_create_from_file('example/example_inputs/django_views.py')
68+
69+
cfg_list = [self.cfg]
70+
funcs = _get_func_nodes()
71+
72+
i = 0
73+
for func in funcs:
74+
if is_django_view_function(func.node):
75+
self.assertIn('view_function', func.node.name)
76+
i = i + 1
77+
# So it is supposed to be 2
78+
self.assertEqual(i, 2)

Diff for: tests/vulnerabilities_test.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pyt.constraint_table import constraint_table, initialize_constraint_table
77
from pyt.fixed_point import analyse
88
from pyt.framework_adaptor import FrameworkAdaptor
9-
from pyt.framework_helper import is_flask_route_function
9+
from pyt.framework_helper import is_flask_route_function, is_django_view_function
1010
from pyt.lattice import Lattice
1111
from pyt.reaching_definitions_taint import ReachingDefinitionsTaintAnalysis
1212

@@ -318,7 +318,7 @@ def test_XSS_url_result(self):
318318
vulnerability_description = str(vulnerability_log.vulnerabilities[0])
319319
EXPECTED_VULNERABILITY_DESCRIPTION = """
320320
File: example/vulnerable_code/XSS_url.py
321-
> User input at line 4, trigger word "Flask function URL parameter":
321+
> User input at line 4, trigger word "Framework function URL parameter":
322322
url
323323
Reassigned in:
324324
File: example/vulnerable_code/XSS_url.py
@@ -454,3 +454,39 @@ def test_XSS_variable_multiple_assign_result(self):
454454
"""
455455

456456
self.assertTrue(self.string_compare_alpha(vulnerability_description, EXPECTED_VULNERABILITY_DESCRIPTION))
457+
458+
459+
class EngineDjangoTest(BaseTestCase):
460+
def run_empty(self):
461+
return
462+
463+
def run_analysis(self, path):
464+
self.cfg_create_from_file(path)
465+
cfg_list = [self.cfg]
466+
467+
FrameworkAdaptor(cfg_list, [], [], is_django_view_function)
468+
initialize_constraint_table(cfg_list)
469+
470+
analyse(cfg_list, analysis_type=ReachingDefinitionsTaintAnalysis)
471+
472+
trigger_word_file = os.path.join('pyt', 'trigger_definitions', 'django_trigger_words.pyt')
473+
474+
return vulnerabilities.find_vulnerabilities(cfg_list, ReachingDefinitionsTaintAnalysis, trigger_word_file=trigger_word_file)
475+
476+
def test_django_view_param(self):
477+
vulnerability_log = self.run_analysis('example/vulnerable_code/django_XSS.py')
478+
self.assert_length(vulnerability_log.vulnerabilities, expected_length=2)
479+
vulnerability_description = str(vulnerability_log.vulnerabilities[0])
480+
481+
EXPECTED_VULNERABILITY_DESCRIPTION = """
482+
File: example/vulnerable_code/django_XSS.py
483+
> User input at line 4, trigger word "Framework function URL parameter":
484+
param
485+
Reassigned in:
486+
File: example/vulnerable_code/django_XSS.py
487+
> Line 5: ret_xss1 = ¤call_1
488+
File: example/vulnerable_code/django_XSS.py
489+
> reaches line 5, trigger word "render(":
490+
¤call_1 = ret_render(request, 'templates/xss.html', 'param'param)
491+
"""
492+
self.assertTrue(self.string_compare_alpha(vulnerability_description, EXPECTED_VULNERABILITY_DESCRIPTION))

0 commit comments

Comments
 (0)