Skip to content

Commit 373807a

Browse files
committed
Merge origin/http-response-url-extension (#3406) into bleeding-edge
2 parents 61469b6 + 9467e68 commit 373807a

4 files changed

Lines changed: 54 additions & 18 deletions

File tree

bbot/core/event/base.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ class BaseEvent:
142142
_discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}")
143143
# Stats class for the status line — override in subclasses for custom formatting
144144
_stats_class = None
145+
# Whether this event is subject to `url_extension_special` distribution filtering.
146+
# False for events representing already-retrieved content, whose consumers analyze the body.
147+
_url_special_filterable = True
145148

146149
# using __slots__ dramatically reduces memory usage in large scans
147150
__slots__ = [
@@ -1164,18 +1167,24 @@ def sanitize_data(self, data):
11641167
class DictEvent(BaseEvent):
11651168
__slots__ = ["url_extension"]
11661169

1170+
def _set_url_extension(self):
1171+
"""Extract the file extension from self.parsed_url and record it as an attribute and a tag."""
1172+
parsed_url = getattr(self, "parsed_url", None)
1173+
if parsed_url is None:
1174+
return
1175+
url_path = parsed_url.path
1176+
if not url_path:
1177+
return
1178+
extension = get_file_extension(str(url_path).lower())
1179+
if extension:
1180+
self.url_extension = extension
1181+
self.add_tag(f"extension-{extension}")
1182+
11671183
def sanitize_data(self, data):
11681184
url = data.get("url", "")
11691185
if url:
11701186
self.parsed_url = self.validators.validate_url_parsed(url)
1171-
# extract url_extension from any dict event with a URL
1172-
url_path = self.parsed_url.path
1173-
if url_path:
1174-
parsed_path_lower = str(url_path).lower()
1175-
extension = get_file_extension(parsed_path_lower)
1176-
if extension:
1177-
self.url_extension = extension
1178-
self.add_tag(f"extension-{extension}")
1187+
self._set_url_extension()
11791188
return data
11801189

11811190
def _data_load(self, data):
@@ -1446,15 +1455,7 @@ def sanitize_data(self, data):
14461455
self.parsed_url = self.validators.validate_url_parsed(url)
14471456
data["url"] = self.parsed_url.geturl()
14481457

1449-
# special handling of URL extensions
1450-
if self.parsed_url is not None:
1451-
url_path = self.parsed_url.path
1452-
if url_path:
1453-
parsed_path_lower = str(url_path).lower()
1454-
extension = get_file_extension(parsed_path_lower)
1455-
if extension:
1456-
self.url_extension = extension
1457-
self.add_tag(f"extension-{extension}")
1458+
self._set_url_extension()
14581459

14591460
# tag as dir or endpoint
14601461
if str(self.parsed_url.path).endswith("/"):
@@ -1716,6 +1717,10 @@ def _words(self):
17161717

17171718

17181719
class HTTP_RESPONSE(URL_UNVERIFIED):
1720+
# the response body has already been retrieved, so content-analysis modules
1721+
# should still receive it even for special extensions like .js
1722+
_url_special_filterable = False
1723+
17191724
def __init__(self, *args, **kwargs):
17201725
super().__init__(*args, **kwargs)
17211726
# count number of consecutive redirects
@@ -1776,6 +1781,7 @@ def sanitize_data(self, data):
17761781
url = data.get("url", "")
17771782
self.parsed_url = self.validators.validate_url_parsed(url)
17781783
data["url"] = self.parsed_url.geturl()
1784+
self._set_url_extension()
17791785

17801786
if not "raw_header" in data:
17811787
raise ValueError("raw_header is required for HTTP_RESPONSE events")

bbot/modules/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,7 @@ def _event_precheck(self, event):
871871
return False, "it did not meet target_only filter criteria"
872872

873873
# limit events with special URL extensions (e.g. .js) to modules that opt in
874-
if not self.accept_url_special:
874+
if not self.accept_url_special and event._url_special_filterable:
875875
extension = getattr(event, "url_extension", "")
876876
if extension in self.scan.url_extension_special:
877877
return (

bbot/test/test_step_1/test_events.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,28 @@ async def test_events(events, helpers):
175175
)
176176
assert getattr(wp_no_ext, "url_extension", "NOT_SET") == "NOT_SET"
177177

178+
# url_extension: HTTP_RESPONSE events
179+
# modules that filter on url_extension (e.g. paramminer, lightfuzz) rely on this being set
180+
def _http_response(url):
181+
return scan.make_event(
182+
{"url": url, "raw_header": "HTTP/1.1 200 OK\r\n\r\n"},
183+
"HTTP_RESPONSE",
184+
dummy=True,
185+
)
186+
187+
hr_pdf = _http_response("https://evilcorp.com/files/document.pdf?foo=bar")
188+
assert getattr(hr_pdf, "url_extension", "") == "pdf"
189+
assert "extension-pdf" in hr_pdf.tags
190+
hr_no_ext = _http_response("https://evilcorp.com/search")
191+
assert getattr(hr_no_ext, "url_extension", "NOT_SET") == "NOT_SET"
192+
193+
# special extensions (.js) must still reach modules that don't opt in to special URLs,
194+
# since the response body has already been retrieved
195+
hr_js = _http_response("https://evilcorp.com/app.js")
196+
assert getattr(hr_js, "url_extension", "") == "js"
197+
assert hr_js._url_special_filterable is False
198+
assert scan.make_event("https://evilcorp.com/app.js", dummy=True)._url_special_filterable is True
199+
178200
# http response
179201
assert events.http_response.host == "example.com"
180202
assert events.http_response.port == 80

bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,11 @@ def check(self, module_test, events):
288288
assert excavate_extracted_param, "Excavate should still extract the parameter from the HTML link"
289289
assert not paramminer_recycled_to_php, "Paramminer should not recycle words from static-URL WEB_PARAMETERs"
290290
assert not paramminer_bruted_pdf, "Paramminer should not brute-force parameters on static URLs"
291+
292+
# paramminer's static filter also applies to HTTP_RESPONSE events, which requires
293+
# url_extension to be populated on them
294+
pdf_responses = [e for e in events if e.type == "HTTP_RESPONSE" and e.data["url"].endswith("test2.pdf")]
295+
assert pdf_responses, "expected an HTTP_RESPONSE for the .pdf URL"
296+
assert getattr(pdf_responses[0], "url_extension", None) == "pdf", (
297+
"HTTP_RESPONSE is missing url_extension, so paramminer's static filter is bypassed"
298+
)

0 commit comments

Comments
 (0)