Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ __pycache__/
build/
dist/
simulation/
*.egg-info/
*.egg-info/
24 changes: 22 additions & 2 deletions adiuvare/signals/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,35 @@ def _bool_taut_hit(text: str) -> bool:
return True
return False

QUESTION_RE = _re.compile(
r"(?i)^(how|what|why|when|can|does|do|is|are)\b.*\?"
)

def is_discussion_context(text: str) -> bool:
return bool(QUESTION_RE.match(text))

EXECUTABLE_SCRIPT_RE = _re.compile(r"(?i)<\s*script\b[^>]*>\s*\S[^<]*<\s*/\s*script\s*>")

def is_executable_xss(text: str) -> bool:
return bool(EXECUTABLE_SCRIPT_RE.search(text))

def should_suppress_xss_lib(text: str) -> bool:
return is_discussion_context(text) and not is_executable_xss(text)

def check_sql(text: str) -> tuple[bool, float, str]:
if _bool_taut_hit(text):
return True, 0.92, "bool_taut"
return _scan(sql_pats, text)
hit = _scan(sql_pats, text)
if hit[2] == "select_from" and is_discussion_context(text):
return False, 0.0, ""
return hit


def check_xss(text: str) -> tuple[bool, float, str]:
return _scan(xss_pats, text)
hit = _scan(xss_pats, text)
if hit[2] == "script_tag" and is_discussion_context(text) and not is_executable_xss(text):
return False, 0.0, ""
return hit


def check_path(text: str) -> tuple[bool, float, str]:
Expand Down
5 changes: 3 additions & 2 deletions adiuvare/signals/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ..core.models import RequestContext, SignalResult
from ..vendor import detect_sqli, detect_xss, normalize
from .base import SoftSignal
from .patterns import check_cmd, check_nosql, check_path, check_sql, check_ssti, check_xss, should_suppress_xss_lib
from .patterns import check_cmd, check_nosql, check_path, check_sql, check_ssti, check_xss

def _is_discussion_style_sql(text: str) -> bool:
Expand Down Expand Up @@ -92,7 +93,7 @@ async def extract(self, ctx: RequestContext) -> SignalResult:
hits.append((max(sql_lib["conf"], 0.82), sql_lib["fp"] or "sql_lib"))
if sql_pat[0]:
hits.append((sql_pat[1], sql_pat[2]))
if xss_lib["hit"]:
if xss_lib["hit"] and not should_suppress_xss_lib(text):
hits.append((max(xss_lib["conf"] * 0.80, 0.62), "xss_lib"))
if xss_pat[0]:
hits.append((xss_pat[1], xss_pat[2]))
Expand Down Expand Up @@ -129,4 +130,4 @@ async def extract(self, ctx: RequestContext) -> SignalResult:
"nosql_pat": nosql_pat[2],
"ldap_pat": ldap_pat[2],
}
return SignalResult(score=score, reason=top[1], detail=detail)
return SignalResult(score=score, reason=top[1], detail=detail)
2 changes: 2 additions & 0 deletions tests/test_benign_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def test_benign_matrix_stays_clean():
"How does $(HOME) expansion work in Bash?",
"render {{ user.name }} in the template",
"Use $gt for greater-than filters in Mongo docs",
"How do I write SELECT * FROM users in a tutorial?",
"How do I print <script> literally in docs?"
]

for text in cases:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ def test_payload_keeps_union_phrase_clean():
res = asyncio.run(PayloadSignal().extract(ctx))
assert res.score == 0.0


def test_payload_does_not_suppress_union_select_in_question():
ctx = RequestContext(
identity="u1",
payload="Can you show me how UNION SELECT password FROM users works?",
def test_payload_keeps_discussion_style_select_example_lower():
ctx = RequestContext(
identity="u1",
Expand Down Expand Up @@ -499,6 +504,16 @@ def test_payload_marks_cn_ldap_injection_probe():
assert res.score >= 0.7


def test_payload_does_not_suppress_executable_script_in_question():
ctx = RequestContext(
identity="u1",
payload="What does <script>document.cookie</script> do?",
url="/comment",
method="POST",
headers={},
ip="127.0.0.1",
endpoint="/comment",
)
def test_payload_marks_mixed_attr_ldap_injection_probe():
ctx = RequestContext(
identity="u1",
Expand All @@ -514,6 +529,10 @@ def test_payload_marks_mixed_attr_ldap_injection_probe():
assert res.score >= 0.7


def test_payload_does_not_suppress_boolean_sqli_in_question():
ctx = RequestContext(
identity="u1",
payload="What happens with ' OR 1=1--?",
def test_payload_marks_encoded_ldap_injection_probe():
ctx = RequestContext(
identity="u1",
Expand Down Expand Up @@ -663,6 +682,8 @@ def test_payload_marks_encoded_top_level_nosql_operator_text():
ip="127.0.0.1",
endpoint="/login",
)
res = asyncio.run(PayloadSignal().extract(ctx))
assert res.score >= 0.7

res = asyncio.run(PayloadSignal().extract(ctx))
assert res.score >= 0.6
Expand Down
Loading