Skip to content

Commit fc82581

Browse files
committed
fix(util): reject all invalid chars during number parsing
1 parent 6870201 commit fc82581

7 files changed

Lines changed: 73 additions & 13 deletions

File tree

httoop/messages/protocol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from httoop.exceptions import InvalidLine
1212
from httoop.meta import Semantic
13-
from httoop.util import _
13+
from httoop.util import _, integer
1414

1515

1616
__all__ = ('Protocol',)
@@ -47,13 +47,13 @@ def set(self, protocol: bytes | Protocol | tuple[int, int] | int | str) -> None:
4747
protocol = self.parse(protocol)
4848
else:
4949
major, minor = tuple(protocol)
50-
self.__protocol = (int(major), int(minor))
50+
self.__protocol = (integer(major), integer(minor))
5151

5252
def parse(self, protocol: bytes) -> None:
5353
match = self.PROTOCOL_RE.match(protocol)
5454
if match is None:
5555
raise InvalidLine(_('Invalid HTTP protocol: %r'), protocol.decode('ISO8859-1'))
56-
self.__protocol = (int(match.group(2)), int(match.group(3)))
56+
self.__protocol = (integer(match.group(2)), integer(match.group(3)))
5757
self.name = match.group(1)
5858

5959
def compose(self) -> bytes:

httoop/status/status.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def parse(self, status: bytes) -> None:
132132
if match is None:
133133
raise InvalidLine(_('Invalid status %r'), status.decode('ISO8859-1'))
134134

135-
self.set((int(match.group(1)), match.group(2).decode('ascii')))
135+
self.set((integer(match.group(1)), match.group(2).decode('ascii')))
136136

137137
def compose(self) -> bytes:
138138
return b'%d %s' % (self.__code, self.__reason.encode('ascii'))

httoop/uri/uri.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def port(self, port) -> None:
6565
if port:
6666
try:
6767
port = integer(port)
68-
if not 0 < integer(port) <= 65535:
68+
if not 0 < port <= 65535:
6969
raise ValueError
7070
except ValueError:
7171
raise InvalidURI(_('Invalid port: %r'), port) # TODO: TypeError

httoop/util.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import codecs
6+
import string
67
from typing import Any, Callable
78

89

@@ -61,18 +62,45 @@ def _decorated(self, *args, **kwargs):
6162
return _decorated
6263

6364

64-
def integer(number: int | str | bytes, *args) -> int:
65+
def integer(number: str | bytes, base=10) -> int:
6566
"""
66-
In Python 3 int() is broken.
67-
>>> int(bytearray(b'1_0'))
67+
The native Python integer parsing from string allows to many forms which are not allowed by the protocol.
68+
69+
>>> integer(bytearray(b'10'))
70+
10
71+
>>> integer(b'5a', 16)
72+
90
73+
>>> integer(bytearray(b'1_0'))
74+
Traceback (most recent call last):
75+
...
76+
ValueError:
77+
>>> integer(bytearray(b' 10'))
78+
Traceback (most recent call last):
79+
...
80+
ValueError:
81+
>>> integer(bytearray(b'\t10'))
82+
Traceback (most recent call last):
83+
...
84+
ValueError:
85+
>>> integer(bytearray(b'\v10'))
86+
Traceback (most recent call last):
87+
...
88+
ValueError:
89+
>>> integer(bytearray(b'\f10'))
90+
Traceback (most recent call last):
91+
...
92+
ValueError:
93+
>>> integer(bytearray(b'10 \t\v\f'))
6894
Traceback (most recent call last):
6995
...
7096
ValueError:
7197
"""
72-
num = int(number, *args)
73-
if (isinstance(number, str) and '_' in number) or (isinstance(number, (bytes, bytearray)) and b' ' in number):
74-
raise ValueError()
75-
return num
98+
if isinstance(number, int):
99+
return int(number)
100+
if not number.isdigit():
101+
if base != 16 or number.strip(string.hexdigits if isinstance(string, str) else string.hexdigits.encode('ASCII')):
102+
raise ValueError()
103+
return int(number, base)
76104

77105

78106
class IFile:

tests/messaging/test_body.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def test_body_deflate_compressed(request_, response, clientstatemachine):
140140
assert str(res.body) == 'this is a test'
141141

142142

143-
@pytest.mark.parametrize('chunk_size', [b'-18', b'fg'])
143+
@pytest.mark.parametrize('chunk_size', [b'-18', b'fg', b'1_8'])
144144
def test_parse_chunked_body_with_invalid_chunk_size(statemachine, chunk_size):
145145
statemachine.parse(b'POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTrailer: Foo\r\nAccept: */*\r\nUser-Agent: httoop/0.0\r\nHost: localhost\r\nContent-Type: text/plain; charset="UTF-8"\r\n\r\n')
146146
with pytest.raises(BAD_REQUEST) as exc:

tests/messaging/test_response_protocol.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import pytest
2+
3+
from httoop.exceptions import InvalidLine
4+
5+
16
def test_response_protocol_with_http1_0_request_():
27
pass
38

@@ -16,3 +21,15 @@ def test_response_protocol_with_http0_9_request_():
1621

1722
def test_response_protocol_with_http2_0_request_():
1823
pass
24+
25+
26+
@pytest.mark.parametrize('protocol', [
27+
b'HTTP/ 1.1',
28+
b'HTTP/1_1.1',
29+
b'HTTP/1.a',
30+
b'HTTP/1.1a',
31+
b'HTTP/1.1\t',
32+
])
33+
def test_invalid_protocol(response, protocol):
34+
with pytest.raises(InvalidLine):
35+
response.protocol.parse(protocol)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import pytest
2+
3+
from httoop.exceptions import InvalidLine
4+
5+
6+
@pytest.mark.parametrize('status', [
7+
b' OK',
8+
b'2_00 OK',
9+
b'2_0 OK',
10+
b' 200 OK',
11+
b'\v200 OK',
12+
])
13+
def test_invalid_status(response, status):
14+
with pytest.raises(InvalidLine):
15+
response.status.parse(status)

0 commit comments

Comments
 (0)