pypproxy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypproxy/__init__.py +0 -0
- pypproxy/api/__init__.py +0 -0
- pypproxy/api/server.py +427 -0
- pypproxy/bulk/__init__.py +0 -0
- pypproxy/bulk/sender.py +97 -0
- pypproxy/cert/__init__.py +0 -0
- pypproxy/cert/ca.py +144 -0
- pypproxy/cert/client_cert.py +65 -0
- pypproxy/codec.py +176 -0
- pypproxy/config/__init__.py +0 -0
- pypproxy/config/config.py +106 -0
- pypproxy/dns/__init__.py +0 -0
- pypproxy/dns/server.py +149 -0
- pypproxy/exporter/__init__.py +0 -0
- pypproxy/exporter/exporter.py +122 -0
- pypproxy/exporter/importer.py +169 -0
- pypproxy/graphql/__init__.py +0 -0
- pypproxy/graphql/detector.py +76 -0
- pypproxy/graphql/introspection.py +217 -0
- pypproxy/graphql/modifier.py +98 -0
- pypproxy/graphql/schema_store.py +33 -0
- pypproxy/intercept/__init__.py +0 -0
- pypproxy/intercept/manager.py +142 -0
- pypproxy/interceptor/__init__.py +0 -0
- pypproxy/interceptor/interceptor.py +172 -0
- pypproxy/proto/__init__.py +0 -0
- pypproxy/proto/grpc.py +48 -0
- pypproxy/proto/mqtt.py +119 -0
- pypproxy/proto/ws.py +120 -0
- pypproxy/proto/ws_intercept.py +117 -0
- pypproxy/proxy/__init__.py +0 -0
- pypproxy/proxy/proxy.py +407 -0
- pypproxy/replay/__init__.py +0 -0
- pypproxy/replay/replay.py +77 -0
- pypproxy/rule/__init__.py +0 -0
- pypproxy/rule/rule.py +198 -0
- pypproxy/scan/__init__.py +0 -0
- pypproxy/scan/scanner.py +296 -0
- pypproxy/script/__init__.py +0 -0
- pypproxy/script/engine.py +49 -0
- pypproxy/security/__init__.py +0 -0
- pypproxy/security/header_checker.py +308 -0
- pypproxy/security/int_overflow.py +193 -0
- pypproxy/security/jwt_checker.py +273 -0
- pypproxy/security/plugin.py +152 -0
- pypproxy/security/randomness.py +165 -0
- pypproxy/store/__init__.py +0 -0
- pypproxy/store/db.py +189 -0
- pypproxy/store/filter_parser.py +181 -0
- pypproxy/store/fts.py +105 -0
- pypproxy/store/models.py +81 -0
- pypproxy/store/scope.py +63 -0
- pypproxy/store/store.py +120 -0
- pypproxy/ui/__init__.py +0 -0
- pypproxy/ui/app.py +386 -0
- pypproxy/ui/bulk_sender_ui.py +125 -0
- pypproxy/ui/cui.py +162 -0
- pypproxy/ui/detail.py +179 -0
- pypproxy/ui/diff_view.py +118 -0
- pypproxy/ui/graphql_tab.py +265 -0
- pypproxy/ui/import_tab.py +136 -0
- pypproxy/ui/intercept_dialog.py +74 -0
- pypproxy/ui/resender.py +140 -0
- pypproxy/ui/scan_tab.py +98 -0
- pypproxy/ui/security_tab.py +356 -0
- pypproxy/ui/settings.py +413 -0
- pypproxy/ui/theme.py +59 -0
- pypproxy-0.1.0.dist-info/METADATA +19 -0
- pypproxy-0.1.0.dist-info/RECORD +72 -0
- pypproxy-0.1.0.dist-info/WHEEL +4 -0
- pypproxy-0.1.0.dist-info/entry_points.txt +2 -0
- pypproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class HeaderCheckResult:
|
|
8
|
+
header: str
|
|
9
|
+
present: bool
|
|
10
|
+
value: str
|
|
11
|
+
passed: bool
|
|
12
|
+
severity: str # info, low, medium, high
|
|
13
|
+
detail: str
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> dict:
|
|
16
|
+
return {
|
|
17
|
+
"header": self.header,
|
|
18
|
+
"present": self.present,
|
|
19
|
+
"value": self.value,
|
|
20
|
+
"passed": self.passed,
|
|
21
|
+
"severity": self.severity,
|
|
22
|
+
"detail": self.detail,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_security_headers(resp_headers: dict[str, list[str]]) -> list[HeaderCheckResult]:
|
|
27
|
+
"""Analyse response headers for security issues."""
|
|
28
|
+
flat = {k.lower(): ", ".join(v) for k, v in resp_headers.items()}
|
|
29
|
+
results: list[HeaderCheckResult] = []
|
|
30
|
+
|
|
31
|
+
results.extend(
|
|
32
|
+
[
|
|
33
|
+
_check_hsts(flat),
|
|
34
|
+
_check_csp(flat),
|
|
35
|
+
_check_x_frame(flat),
|
|
36
|
+
_check_x_content_type(flat),
|
|
37
|
+
_check_x_xss(flat),
|
|
38
|
+
_check_referrer_policy(flat),
|
|
39
|
+
_check_permissions_policy(flat),
|
|
40
|
+
_check_cors(flat),
|
|
41
|
+
_check_server(flat),
|
|
42
|
+
_check_x_powered_by(flat),
|
|
43
|
+
_check_cache_control(flat),
|
|
44
|
+
_check_set_cookie(flat),
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return results
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _check_hsts(h: dict) -> HeaderCheckResult:
|
|
52
|
+
key = "strict-transport-security"
|
|
53
|
+
val = h.get(key, "")
|
|
54
|
+
if not val:
|
|
55
|
+
return HeaderCheckResult(
|
|
56
|
+
key,
|
|
57
|
+
False,
|
|
58
|
+
"",
|
|
59
|
+
False,
|
|
60
|
+
"high",
|
|
61
|
+
"Missing HSTS header. Clients may connect over plain HTTP.",
|
|
62
|
+
)
|
|
63
|
+
max_age = 0
|
|
64
|
+
for part in val.split(";"):
|
|
65
|
+
part = part.strip().lower()
|
|
66
|
+
if part.startswith("max-age="):
|
|
67
|
+
import contextlib
|
|
68
|
+
|
|
69
|
+
with contextlib.suppress(ValueError):
|
|
70
|
+
max_age = int(part.split("=")[1])
|
|
71
|
+
passed = max_age >= 31536000
|
|
72
|
+
return HeaderCheckResult(
|
|
73
|
+
key,
|
|
74
|
+
True,
|
|
75
|
+
val,
|
|
76
|
+
passed,
|
|
77
|
+
"medium" if not passed else "info",
|
|
78
|
+
f"max-age={max_age} ({'≥1 year OK' if passed else '<1 year, consider increasing'})",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _check_csp(h: dict) -> HeaderCheckResult:
|
|
83
|
+
key = "content-security-policy"
|
|
84
|
+
val = h.get(key, "")
|
|
85
|
+
if not val:
|
|
86
|
+
return HeaderCheckResult(
|
|
87
|
+
key, False, "", False, "high", "Missing CSP. XSS attacks are not mitigated."
|
|
88
|
+
)
|
|
89
|
+
issues = []
|
|
90
|
+
if "unsafe-inline" in val:
|
|
91
|
+
issues.append("'unsafe-inline' allows inline scripts")
|
|
92
|
+
if "unsafe-eval" in val:
|
|
93
|
+
issues.append("'unsafe-eval' allows eval()")
|
|
94
|
+
if "default-src *" in val or "script-src *" in val:
|
|
95
|
+
issues.append("Wildcard source too permissive")
|
|
96
|
+
passed = len(issues) == 0
|
|
97
|
+
detail = "OK" if passed else "; ".join(issues)
|
|
98
|
+
return HeaderCheckResult(
|
|
99
|
+
key, True, val[:80], passed, "medium" if not passed else "info", detail
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _check_x_frame(h: dict) -> HeaderCheckResult:
|
|
104
|
+
key = "x-frame-options"
|
|
105
|
+
val = h.get(key, "")
|
|
106
|
+
if not val:
|
|
107
|
+
# check CSP frame-ancestors instead
|
|
108
|
+
csp = h.get("content-security-policy", "")
|
|
109
|
+
if "frame-ancestors" in csp:
|
|
110
|
+
return HeaderCheckResult(
|
|
111
|
+
key,
|
|
112
|
+
False,
|
|
113
|
+
"",
|
|
114
|
+
True,
|
|
115
|
+
"info",
|
|
116
|
+
"Not present, but CSP frame-ancestors found (acceptable)",
|
|
117
|
+
)
|
|
118
|
+
return HeaderCheckResult(
|
|
119
|
+
key,
|
|
120
|
+
False,
|
|
121
|
+
"",
|
|
122
|
+
False,
|
|
123
|
+
"medium",
|
|
124
|
+
"Missing X-Frame-Options. Clickjacking may be possible.",
|
|
125
|
+
)
|
|
126
|
+
val_up = val.upper()
|
|
127
|
+
passed = "DENY" in val_up or "SAMEORIGIN" in val_up
|
|
128
|
+
return HeaderCheckResult(
|
|
129
|
+
key,
|
|
130
|
+
True,
|
|
131
|
+
val,
|
|
132
|
+
passed,
|
|
133
|
+
"info" if passed else "medium",
|
|
134
|
+
"OK" if passed else f"Unexpected value: {val}",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_x_content_type(h: dict) -> HeaderCheckResult:
|
|
139
|
+
key = "x-content-type-options"
|
|
140
|
+
val = h.get(key, "")
|
|
141
|
+
passed = val.lower() == "nosniff"
|
|
142
|
+
if not val:
|
|
143
|
+
return HeaderCheckResult(
|
|
144
|
+
key, False, "", False, "low", "Missing. Browser may MIME-sniff responses."
|
|
145
|
+
)
|
|
146
|
+
return HeaderCheckResult(
|
|
147
|
+
key,
|
|
148
|
+
True,
|
|
149
|
+
val,
|
|
150
|
+
passed,
|
|
151
|
+
"info" if passed else "low",
|
|
152
|
+
"OK" if passed else f"Should be 'nosniff', got: {val}",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _check_x_xss(h: dict) -> HeaderCheckResult:
|
|
157
|
+
key = "x-xss-protection"
|
|
158
|
+
val = h.get(key, "")
|
|
159
|
+
if not val:
|
|
160
|
+
return HeaderCheckResult(
|
|
161
|
+
key,
|
|
162
|
+
False,
|
|
163
|
+
"",
|
|
164
|
+
True,
|
|
165
|
+
"info",
|
|
166
|
+
"Not present (modern browsers ignore this; rely on CSP instead)",
|
|
167
|
+
)
|
|
168
|
+
passed = "1; mode=block" in val or val == "0"
|
|
169
|
+
return HeaderCheckResult(
|
|
170
|
+
key, True, val, passed, "info", "OK" if passed else f"Unusual value: {val}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_referrer_policy(h: dict) -> HeaderCheckResult:
|
|
175
|
+
key = "referrer-policy"
|
|
176
|
+
val = h.get(key, "")
|
|
177
|
+
safe = {
|
|
178
|
+
"no-referrer",
|
|
179
|
+
"no-referrer-when-downgrade",
|
|
180
|
+
"strict-origin",
|
|
181
|
+
"strict-origin-when-cross-origin",
|
|
182
|
+
}
|
|
183
|
+
passed = any(s in val.lower() for s in safe)
|
|
184
|
+
if not val:
|
|
185
|
+
return HeaderCheckResult(
|
|
186
|
+
key, False, "", False, "low", "Missing. Full URL may be leaked in Referer header."
|
|
187
|
+
)
|
|
188
|
+
return HeaderCheckResult(
|
|
189
|
+
key,
|
|
190
|
+
True,
|
|
191
|
+
val,
|
|
192
|
+
passed,
|
|
193
|
+
"info" if passed else "low",
|
|
194
|
+
"OK" if passed else f"Consider stricter policy, got: {val}",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _check_permissions_policy(h: dict) -> HeaderCheckResult:
|
|
199
|
+
key = "permissions-policy"
|
|
200
|
+
val = h.get(key, h.get("feature-policy", ""))
|
|
201
|
+
present = bool(val)
|
|
202
|
+
return HeaderCheckResult(
|
|
203
|
+
key,
|
|
204
|
+
present,
|
|
205
|
+
val[:80] if val else "",
|
|
206
|
+
present,
|
|
207
|
+
"info" if present else "low",
|
|
208
|
+
"OK" if present else "Consider adding Permissions-Policy to restrict browser features",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _check_cors(h: dict) -> HeaderCheckResult:
|
|
213
|
+
key = "access-control-allow-origin"
|
|
214
|
+
val = h.get(key, "")
|
|
215
|
+
if not val:
|
|
216
|
+
return HeaderCheckResult(
|
|
217
|
+
key, False, "", True, "info", "Not present (CORS not enabled for this endpoint)"
|
|
218
|
+
)
|
|
219
|
+
passed = val != "*"
|
|
220
|
+
return HeaderCheckResult(
|
|
221
|
+
key,
|
|
222
|
+
True,
|
|
223
|
+
val,
|
|
224
|
+
passed,
|
|
225
|
+
"high" if not passed else "info",
|
|
226
|
+
"Wildcard ACAO allows cross-origin access from any domain" if not passed else "OK",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _check_server(h: dict) -> HeaderCheckResult:
|
|
231
|
+
key = "server"
|
|
232
|
+
val = h.get(key, "")
|
|
233
|
+
if not val:
|
|
234
|
+
return HeaderCheckResult(key, False, "", True, "info", "Server header not exposed (good)")
|
|
235
|
+
# Check for version disclosure
|
|
236
|
+
import re
|
|
237
|
+
|
|
238
|
+
has_version = bool(re.search(r"[0-9]+\.[0-9]+", val))
|
|
239
|
+
passed = not has_version
|
|
240
|
+
return HeaderCheckResult(
|
|
241
|
+
key,
|
|
242
|
+
True,
|
|
243
|
+
val,
|
|
244
|
+
passed,
|
|
245
|
+
"low" if not passed else "info",
|
|
246
|
+
"Version number disclosed in Server header" if not passed else "No version disclosed",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _check_x_powered_by(h: dict) -> HeaderCheckResult:
|
|
251
|
+
key = "x-powered-by"
|
|
252
|
+
val = h.get(key, "")
|
|
253
|
+
passed = not val
|
|
254
|
+
return HeaderCheckResult(
|
|
255
|
+
key,
|
|
256
|
+
bool(val),
|
|
257
|
+
val,
|
|
258
|
+
passed,
|
|
259
|
+
"low" if not passed else "info",
|
|
260
|
+
"Technology stack disclosed" if not passed else "Not present (good)",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _check_cache_control(h: dict) -> HeaderCheckResult:
|
|
265
|
+
key = "cache-control"
|
|
266
|
+
val = h.get(key, "")
|
|
267
|
+
if not val:
|
|
268
|
+
return HeaderCheckResult(
|
|
269
|
+
key,
|
|
270
|
+
False,
|
|
271
|
+
"",
|
|
272
|
+
False,
|
|
273
|
+
"low",
|
|
274
|
+
"Missing Cache-Control. Sensitive responses may be cached.",
|
|
275
|
+
)
|
|
276
|
+
sensitive_ok = "no-store" in val or "private" in val
|
|
277
|
+
return HeaderCheckResult(
|
|
278
|
+
key,
|
|
279
|
+
True,
|
|
280
|
+
val,
|
|
281
|
+
True,
|
|
282
|
+
"info",
|
|
283
|
+
f"{'Contains no-store/private' if sensitive_ok else 'Consider no-store for sensitive endpoints'}",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _check_set_cookie(h: dict) -> HeaderCheckResult:
|
|
288
|
+
key = "set-cookie"
|
|
289
|
+
val = h.get(key, "")
|
|
290
|
+
if not val:
|
|
291
|
+
return HeaderCheckResult(key, False, "", True, "info", "No Set-Cookie header")
|
|
292
|
+
issues = []
|
|
293
|
+
val_lower = val.lower()
|
|
294
|
+
if "secure" not in val_lower:
|
|
295
|
+
issues.append("Missing 'Secure' flag (cookie sent over HTTP)")
|
|
296
|
+
if "httponly" not in val_lower:
|
|
297
|
+
issues.append("Missing 'HttpOnly' flag (accessible via JavaScript)")
|
|
298
|
+
if "samesite" not in val_lower:
|
|
299
|
+
issues.append("Missing 'SameSite' attribute (CSRF risk)")
|
|
300
|
+
passed = len(issues) == 0
|
|
301
|
+
return HeaderCheckResult(
|
|
302
|
+
key,
|
|
303
|
+
True,
|
|
304
|
+
val[:80],
|
|
305
|
+
passed,
|
|
306
|
+
"medium" if not passed else "info",
|
|
307
|
+
"; ".join(issues) if issues else "OK",
|
|
308
|
+
)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import parse_qs, urlencode
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from pypproxy.store.models import Entry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class OverflowPayload:
|
|
17
|
+
label: str
|
|
18
|
+
description: str
|
|
19
|
+
value: Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class OverflowResult:
|
|
24
|
+
param: str
|
|
25
|
+
payload: OverflowPayload
|
|
26
|
+
status_code: int = 0
|
|
27
|
+
response_body: bytes = b""
|
|
28
|
+
duration_ms: int = 0
|
|
29
|
+
error: str = ""
|
|
30
|
+
suspicious: bool = False
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
import base64
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
"param": self.param,
|
|
37
|
+
"label": self.payload.label,
|
|
38
|
+
"description": self.payload.description,
|
|
39
|
+
"value": str(self.payload.value),
|
|
40
|
+
"status_code": self.status_code,
|
|
41
|
+
"response_body": base64.b64encode(self.response_body).decode()
|
|
42
|
+
if self.response_body
|
|
43
|
+
else "",
|
|
44
|
+
"duration_ms": self.duration_ms,
|
|
45
|
+
"error": self.error,
|
|
46
|
+
"suspicious": self.suspicious,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_INT_PAYLOADS = [
|
|
51
|
+
OverflowPayload("plus_one", "+1 from original", None), # filled dynamically
|
|
52
|
+
OverflowPayload("minus_one", "-1 from original", None),
|
|
53
|
+
OverflowPayload("zero", "Zero value", 0),
|
|
54
|
+
OverflowPayload("negative", "Large negative", -2147483648),
|
|
55
|
+
OverflowPayload("max_int32", "Max int32", 2147483647),
|
|
56
|
+
OverflowPayload("max_int32_plus1", "Max int32 + 1 (overflow)", 2147483648),
|
|
57
|
+
OverflowPayload("max_int64", "Max int64", 9223372036854775807),
|
|
58
|
+
OverflowPayload("max_uint32", "Max uint32", 4294967295),
|
|
59
|
+
OverflowPayload("long_num", "Very long number", 99999999999999999999),
|
|
60
|
+
OverflowPayload("float", "Float (0.1)", 0.1),
|
|
61
|
+
OverflowPayload("neg_float", "Negative float", -0.1),
|
|
62
|
+
OverflowPayload("sci_notation", "Scientific notation", "1e308"),
|
|
63
|
+
OverflowPayload("nan", "NaN string", "NaN"),
|
|
64
|
+
OverflowPayload("inf", "Infinity string", "Infinity"),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_int_params(entry: Entry) -> dict[str, int]:
|
|
69
|
+
"""Find integer-valued parameters in query string and JSON body."""
|
|
70
|
+
params: dict[str, int] = {}
|
|
71
|
+
|
|
72
|
+
# query string
|
|
73
|
+
if entry.query:
|
|
74
|
+
import contextlib
|
|
75
|
+
|
|
76
|
+
for k, vs in parse_qs(entry.query).items():
|
|
77
|
+
for v in vs:
|
|
78
|
+
with contextlib.suppress(ValueError):
|
|
79
|
+
params[f"query:{k}"] = int(v)
|
|
80
|
+
|
|
81
|
+
# JSON body
|
|
82
|
+
if entry.req_body:
|
|
83
|
+
try:
|
|
84
|
+
data = json.loads(entry.req_body.decode("utf-8", errors="replace"))
|
|
85
|
+
_collect_ints(data, "", params)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return params
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _collect_ints(obj: Any, prefix: str, out: dict[str, int]) -> None:
|
|
93
|
+
if isinstance(obj, dict):
|
|
94
|
+
for k, v in obj.items():
|
|
95
|
+
_collect_ints(v, f"body:{prefix}{k}", out)
|
|
96
|
+
elif isinstance(obj, list):
|
|
97
|
+
for i, v in enumerate(obj):
|
|
98
|
+
_collect_ints(v, f"{prefix}[{i}].", out)
|
|
99
|
+
elif isinstance(obj, int) and not isinstance(obj, bool):
|
|
100
|
+
out[prefix.rstrip(".")] = obj
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _apply_to_query(query: str, param_key: str, new_value: Any) -> str:
|
|
104
|
+
key = param_key.removeprefix("query:")
|
|
105
|
+
qs = parse_qs(query)
|
|
106
|
+
qs[key] = [str(new_value)]
|
|
107
|
+
return urlencode(qs, doseq=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _apply_to_body(body: bytes, param_key: str, new_value: Any) -> bytes:
|
|
111
|
+
key_path = param_key.removeprefix("body:")
|
|
112
|
+
try:
|
|
113
|
+
data = json.loads(body.decode("utf-8", errors="replace"))
|
|
114
|
+
_set_nested(data, key_path, new_value)
|
|
115
|
+
return json.dumps(data).encode()
|
|
116
|
+
except Exception:
|
|
117
|
+
return body
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _set_nested(obj: Any, key_path: str, value: Any) -> None:
|
|
121
|
+
parts = re.split(r"[.\[\]]", key_path)
|
|
122
|
+
parts = [p for p in parts if p]
|
|
123
|
+
for part in parts[:-1]:
|
|
124
|
+
if isinstance(obj, dict):
|
|
125
|
+
obj = obj.get(part, {})
|
|
126
|
+
elif isinstance(obj, list):
|
|
127
|
+
obj = obj[int(part)]
|
|
128
|
+
last = parts[-1]
|
|
129
|
+
if isinstance(obj, dict):
|
|
130
|
+
obj[last] = value
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def run_checks(entry: Entry, timeout: int = 30) -> list[OverflowResult]:
|
|
134
|
+
int_params = _extract_int_params(entry)
|
|
135
|
+
if not int_params:
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
results: list[OverflowResult] = []
|
|
139
|
+
url_base = f"{entry.scheme}://{entry.host}{entry.path}"
|
|
140
|
+
req_headers = {k: ", ".join(v) for k, v in entry.req_headers.items()}
|
|
141
|
+
|
|
142
|
+
for param_key, original_value in int_params.items():
|
|
143
|
+
payloads = []
|
|
144
|
+
for p in _INT_PAYLOADS:
|
|
145
|
+
pl = OverflowPayload(p.label, p.description, p.value)
|
|
146
|
+
if p.label == "plus_one":
|
|
147
|
+
pl.value = original_value + 1
|
|
148
|
+
elif p.label == "minus_one":
|
|
149
|
+
pl.value = original_value - 1
|
|
150
|
+
payloads.append(pl)
|
|
151
|
+
|
|
152
|
+
for payload in payloads:
|
|
153
|
+
if param_key.startswith("query:"):
|
|
154
|
+
new_query = _apply_to_query(entry.query, param_key, payload.value)
|
|
155
|
+
url = url_base + ("?" + new_query if new_query else "")
|
|
156
|
+
req_body = entry.req_body
|
|
157
|
+
else:
|
|
158
|
+
url = url_base + ("?" + entry.query if entry.query else "")
|
|
159
|
+
req_body = _apply_to_body(entry.req_body, param_key, payload.value)
|
|
160
|
+
|
|
161
|
+
start = time.monotonic()
|
|
162
|
+
try:
|
|
163
|
+
async with httpx.AsyncClient(verify=False, timeout=timeout, http2=True) as client:
|
|
164
|
+
resp = await client.request(
|
|
165
|
+
method=entry.method,
|
|
166
|
+
url=url,
|
|
167
|
+
headers=req_headers,
|
|
168
|
+
content=req_body,
|
|
169
|
+
)
|
|
170
|
+
status_code = resp.status_code
|
|
171
|
+
resp_body = resp.content[:512]
|
|
172
|
+
error = ""
|
|
173
|
+
suspicious = status_code in (500, 502, 503)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
status_code = 0
|
|
176
|
+
resp_body = b""
|
|
177
|
+
error = str(e)
|
|
178
|
+
suspicious = False
|
|
179
|
+
|
|
180
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
181
|
+
results.append(
|
|
182
|
+
OverflowResult(
|
|
183
|
+
param=param_key,
|
|
184
|
+
payload=payload,
|
|
185
|
+
status_code=status_code,
|
|
186
|
+
response_body=resp_body,
|
|
187
|
+
duration_ms=dur,
|
|
188
|
+
error=error,
|
|
189
|
+
suspicious=suspicious,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return results
|