Merge lp:~jderose/degu/dotdot into lp:degu

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 479
Proposed branch: lp:~jderose/degu/dotdot
Merge into: lp:degu
Diff against target: 152 lines (+75/-1)
5 files modified
degu/_base.c (+5/-0)
degu/_basepy.py (+2/-0)
degu/tests/helpers.py (+1/-1)
degu/tests/test_base.py (+48/-0)
doc/changelog.rst (+19/-0)
To merge this branch: bzr merge lp:~jderose/degu/dotdot
Reviewer Review Type Date Requested Status
David Jordan Approve
Review via email: mp+376982@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
David Jordan (dmj726) wrote :

Great, good security catch!

review: Approve
Revision history for this message
David Jordan (dmj726) wrote :

We should be peachy now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'degu/_base.c'
2--- degu/_base.c 2018-01-13 23:29:31 +0000
3+++ degu/_base.c 2019-12-19 05:05:49 +0000
4@@ -167,6 +167,7 @@
5 _DEGU_SRC_CONSTANT(MINUS, "-")
6 _DEGU_SRC_CONSTANT(SEMICOLON, ";")
7 _DEGU_SRC_CONSTANT(EQUALS, "=")
8+_DEGU_SRC_CONSTANT(DOTDOT, "..")
9
10
11 /******************************************************************************
12@@ -1789,6 +1790,10 @@
13 static inline PyObject *
14 _parse_path_component(DeguSrc src)
15 {
16+ if (_equal(src, DOTDOT)) {
17+ _value_error("bad path component: %R", src);
18+ return NULL;
19+ }
20 return _decode(src, PATH_MASK, "bad bytes in path component: %R");
21 }
22
23
24=== modified file 'degu/_basepy.py'
25--- degu/_basepy.py 2016-10-16 18:32:04 +0000
26+++ degu/_basepy.py 2019-12-19 05:05:49 +0000
27@@ -605,6 +605,8 @@
28
29
30 def _parse_path_component(src):
31+ if src == b'..':
32+ raise ValueError('bad path component: {!r}'.format(src))
33 if PATH.issuperset(src):
34 return src.decode('ascii')
35 raise ValueError('bad bytes in path component: {!r}'.format(src))
36
37=== modified file 'degu/tests/helpers.py'
38--- degu/tests/helpers.py 2019-12-17 03:30:29 +0000
39+++ degu/tests/helpers.py 2019-12-19 05:05:49 +0000
40@@ -118,8 +118,8 @@
41 assert isinstance(allowed, bytes)
42 not_allowed = tables.invert(allowed)
43 for i in range(len(good)):
44+ bad = bytearray(good)
45 for b in not_allowed:
46- bad = bytearray(good)
47 bad[i] = b
48 yield bytes(bad)
49
50
51=== modified file 'degu/tests/test_base.py'
52--- degu/tests/test_base.py 2019-11-26 15:57:14 +0000
53+++ degu/tests/test_base.py 2019-12-19 05:05:49 +0000
54@@ -1750,6 +1750,11 @@
55 self.assertEqual(r.path, [])
56 self.assertIsNone(r.query)
57
58+ # Directory traversal:
59+ with self.assertRaises(ValueError) as cm:
60+ parse_request(b'GET /foo/../bar HTTP/1.1', rfile)
61+ self.assertEqual(str(cm.exception), "bad path component: b'..'")
62+
63 r = parse_request(b'GET / HTTP/1.1\r\nRange: bytes=17-20', rfile)
64 self.assertIs(type(r), Request)
65 self.assertEqual(r.method, 'GET')
66@@ -3144,6 +3149,49 @@
67 parse_uri(b'foo')
68 self.assertEqual(str(cm.exception), "path[0:1] != b'/': b'foo'")
69
70+ # Bad bytes in URI:
71+ good = b'/foo/bar?k=v&a=b'
72+ for bad in iter_bad(good, bytes(_basepy.URI)):
73+ with self.assertRaises(ValueError) as cm:
74+ parse_uri(bad)
75+ self.assertEqual(str(cm.exception),
76+ 'bad bytes in uri: {!r}'.format(bad)
77+ )
78+ with self.assertRaises(ValueError) as cm:
79+ parse_uri(b'/hello /world')
80+ self.assertEqual(str(cm.exception),
81+ "bad bytes in uri: b'/hello /world'"
82+ )
83+
84+ # dot-dot (directory traversal attack):
85+ dotdots = (
86+ b'/..',
87+ b'/../',
88+ b'/../foo',
89+ b'/../foo/',
90+ b'/foo/..',
91+ b'/foo/../',
92+ b'/foo/../bar',
93+ b'/foo/../bar/',
94+ )
95+ for bad in dotdots:
96+ with self.assertRaises(ValueError) as cm:
97+ parse_uri(bad)
98+ self.assertEqual(str(cm.exception), "bad path component: b'..'")
99+ # '..' is allowed, just not as a path component:
100+ self.assertEqual(parse_uri(b'/hello..'),
101+ ('/hello..', [], ['hello..'], None)
102+ )
103+ self.assertEqual(parse_uri(b'/..hello'),
104+ ('/..hello', [], ['..hello'], None)
105+ )
106+ self.assertEqual(parse_uri(b'/hello..world'),
107+ ('/hello..world', [], ['hello..world'], None)
108+ )
109+ self.assertEqual(parse_uri(b'/...'),
110+ ('/...', [], ['...'], None)
111+ )
112+
113 # Empty path component:
114 double_slashers = (
115 b'//',
116
117=== modified file 'doc/changelog.rst'
118--- doc/changelog.rst 2019-12-16 23:36:50 +0000
119+++ doc/changelog.rst 2019-12-19 05:05:49 +0000
120@@ -8,6 +8,24 @@
121
122 `Download Degu 0.19`_
123
124+Bug fixes:
125+
126+ * `lp:1856935`_ --- To prevent directory traversal attacks and similar
127+ mischief, the Degu server now disallows ``'..'`` in request path
128+ components.
129+
130+ For example, these URI will now be rejected (connection will be
131+ closed)::
132+
133+ /..
134+ /foo/../bar
135+
136+ However, ``'..'`` can occur as a sub-string, just not as the entirety of
137+ a path component. For example, these URI are still fine::
138+
139+ /hello..world
140+ /foo/bar..
141+
142
143 New API additions:
144
145@@ -2011,6 +2029,7 @@
146 .. _`Download Degu 0.6`: https://launchpad.net/degu/+milestone/0.6
147 .. _`Download Degu 0.5`: https://launchpad.net/degu/+milestone/0.5
148
149+.. _`lp:1856935`: https://bugs.launchpad.net/degu/+bug/1856935
150 .. _`lp:1590459`: https://bugs.launchpad.net/degu/+bug/1590459
151
152 .. _`HTTPConnection.request()`: https://docs.python.org/3/library/http.client.html#http.client.HTTPConnection.request

Subscribers

People subscribed via source and target branches