diff --git a/badsecrets/base.py b/badsecrets/base.py
index f5a5e7e..5c9ff7b 100644
--- a/badsecrets/base.py
+++ b/badsecrets/base.py
@@ -190,21 +190,25 @@ def _carve_body(self, body, cookies, headers, **kwargs):
"""Extract secrets from HTML body text. Override in subclasses for custom body carving."""
results = []
if self.carve_regex():
- s = re.search(self.carve_regex(), body)
- if s:
- if not self.validate_carve or self.identify(s.groups()[0]):
- r = self.carve_to_check_secret(
- s, url=kwargs.get("url"), body=body, cookies=cookies, headers=headers
- )
- if r:
- r["type"] = "SecretFound"
- else:
- r = {"type": "IdentifyOnly"}
- r["hashcat"] = self._safe_hashcat(s.groups()[0])
- if "product" not in r:
- r["product"] = self.get_product_from_carve(s)
- r["location"] = "body"
- results.append(r)
+ # Walk every match rather than only the first. A page can carry several fields the
+ # carve regex matches -- an empty or decoy __VIEWSTATE alongside a real payload --
+ # and stopping at match #1 lets whichever appears first in the markup hide the rest.
+ for s in re.finditer(self.carve_regex(), body):
+ if self.validate_carve and not self.identify(s.groups()[0]):
+ continue
+ r = self.carve_to_check_secret(s, url=kwargs.get("url"), body=body, cookies=cookies, headers=headers)
+ if r:
+ r["type"] = "SecretFound"
+ else:
+ r = {"type": "IdentifyOnly"}
+ r["hashcat"] = self._safe_hashcat(s.groups()[0])
+ if "product" not in r:
+ r["product"] = self.get_product_from_carve(s)
+ r["location"] = "body"
+ results.append(r)
+ # First candidate that identifies wins, keeping the one-result-per-body
+ # contract and avoiding repeat check_secret() work on expensive modules.
+ break
return results
@classmethod
diff --git a/badsecrets/modules/passive/aspnet_compressedviewstate.py b/badsecrets/modules/passive/aspnet_compressedviewstate.py
index 2207403..fc47e2d 100644
--- a/badsecrets/modules/passive/aspnet_compressedviewstate.py
+++ b/badsecrets/modules/passive/aspnet_compressedviewstate.py
@@ -12,13 +12,17 @@ class ASPNET_compressedviewstate(BadsecretsBase):
yara_carve_rule = (
"rule ASPNET_compressedviewstate_carve {"
' strings: $vs = "__VIEWSTATE" $vstate = "__VSTATE" $cvs = "__COMPRESSEDVIEWSTATE"'
- " condition: $vs or $vstate or $cvs }"
+ ' $cvs_u = "__COMPRESSED_VSTATE"'
+ " condition: $vs or $vstate or $cvs or $cvs_u }"
)
description = {"product": "ASP.NET Compressed Viewstate", "secret": "unprotected", "severity": "CRITICAL"}
carve_locations = ("body",)
def carve_regex(self):
- return re.compile(r"]+__(?:VIEWSTATE|VSTATE|COMPRESSEDVIEWSTATE)\"\s*value=\"(.*?)\"")
+ return re.compile(
+ r"]*__(?:COMPRESSEDVIEWSTATE|COMPRESSED_VSTATE|VIEWSTATE|VSTATE)\")"
+ r"[^>]*?\svalue=\"(.*?)\""
+ )
def check_secret(self, compressed_viewstate):
if not self.identify(compressed_viewstate):
diff --git a/badsecrets/modules/passive/jsf_viewstate.py b/badsecrets/modules/passive/jsf_viewstate.py
index 0083c27..4346690 100644
--- a/badsecrets/modules/passive/jsf_viewstate.py
+++ b/badsecrets/modules/passive/jsf_viewstate.py
@@ -28,7 +28,7 @@ class Jsf_viewstate(BadsecretsBase):
carve_locations = ("body",)
def carve_regex(self):
- return re.compile(r"]*name=\"javax\.faces\.ViewState\")[^>]*?\svalue=\"([^\"]*)\"")
# Mojarra 1.2.x - 2.0.3
def DES3_decrypt(self, ct, password):
diff --git a/tests/aspnet_compressedviewstate_test.py b/tests/aspnet_compressedviewstate_test.py
index 68f5d6a..6fd90b8 100644
--- a/tests/aspnet_compressedviewstate_test.py
+++ b/tests/aspnet_compressedviewstate_test.py
@@ -1,4 +1,5 @@
import gzip
+import time
import base64
from badsecrets import modules_loaded
@@ -89,6 +90,76 @@ def test_aspnet_compressedviewstate_carve_compressedviewstate_field():
assert results[0]["type"] == "SecretFound"
+def test_aspnet_compressedviewstate_carve_compressed_vstate_field():
+ """Carve from the __COMPRESSED_VSTATE (underscore) hidden field."""
+ body = f''
+ x = ASPNETcompressedviewstate()
+ results = x.carve(body=body)
+ assert len(results) > 0
+ assert results[0]["type"] == "SecretFound"
+ assert results[0]["location"] == "body"
+
+
+def test_aspnet_compressedviewstate_carve_not_shadowed_by_empty_viewstate():
+ """An empty __VIEWSTATE field must not hide a real payload, in either document order."""
+ payload = f''
+ empty = ''
+ x = ASPNETcompressedviewstate()
+ for body in (payload + empty, empty + payload):
+ results = x.carve(body=body)
+ secret_results = [r for r in results if r["type"] == "SecretFound"]
+ assert len(secret_results) == 1, f"missed payload for body order: {body[:60]}"
+ assert secret_results[0]["product"] == KNOWN_GOOD
+
+
+def test_aspnet_compressedviewstate_carve_all_modules_compressed_vstate():
+ """__COMPRESSED_VSTATE next to an empty __VIEWSTATE, as seen in the wild."""
+ body = (
+ '"
+ )
+ results = carve_all_modules(body=body)
+ found = [r for r in results if r["detecting_module"] == "ASPNET_compressedviewstate"]
+ assert len(found) == 1
+ assert found[0]["type"] == "SecretFound"
+ assert found[0]["description"]["severity"] == "CRITICAL"
+
+
+def test_aspnet_compressedviewstate_carve_attribute_between_name_and_value():
+ """Unrelated attributes between name= and value= must not defeat the carve."""
+ body = f''
+ x = ASPNETcompressedviewstate()
+ results = x.carve(body=body)
+ assert len(results) > 0
+ assert results[0]["type"] == "SecretFound"
+
+
+def test_aspnet_compressedviewstate_carve_does_not_cross_tag_boundary():
+ """A name in one tag must not pair with a value in the next."""
+ body = ''
+ x = ASPNETcompressedviewstate()
+ assert x.carve(body=body) == []
+
+
+def test_aspnet_compressedviewstate_carve_regex_no_catastrophic_backtracking():
+ """Many name hits in one unclosed tag must stay linear, not O(n^2)."""
+ body = "'
+ x = ASPNETcompressedviewstate()
+ assert x.carve(body=body) == []
+
+
def test_aspnet_compressedviewstate_carve_bad_value():
"""Carve with a non-compressed value in the field should not return SecretFound."""
body = ''