|
| 1 | +import os |
| 2 | +import re |
| 3 | + |
| 4 | +from ..._constants import IAST_SPAN_TAGS |
| 5 | +from .. import oce |
| 6 | +from .._iast_request_context import set_iast_stacktrace_reported |
| 7 | +from .._metrics import _set_metric_iast_executed_sink |
| 8 | +from .._metrics import increment_iast_span_metric |
| 9 | +from .._taint_tracking._errors import iast_taint_log_error |
| 10 | +from ..constants import HTML_TAGS_REMOVE |
| 11 | +from ..constants import STACKTRACE_EXCEPTION_REGEX |
| 12 | +from ..constants import STACKTRACE_FILE_LINE |
| 13 | +from ..constants import VULN_STACKTRACE_LEAK |
| 14 | +from ..taint_sinks._base import VulnerabilityBase |
| 15 | + |
| 16 | + |
| 17 | +@oce.register |
| 18 | +class StacktraceLeak(VulnerabilityBase): |
| 19 | + vulnerability_type = VULN_STACKTRACE_LEAK |
| 20 | + skip_location = True |
| 21 | + |
| 22 | + |
| 23 | +def asm_report_stacktrace_leak_from_django_debug_page(exc_name, module): |
| 24 | + increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, StacktraceLeak.vulnerability_type) |
| 25 | + _set_metric_iast_executed_sink(StacktraceLeak.vulnerability_type) |
| 26 | + evidence = "Module: %s\nException: %s" % (module, exc_name) |
| 27 | + StacktraceLeak.report(evidence_value=evidence) |
| 28 | + set_iast_stacktrace_reported(True) |
| 29 | + |
| 30 | + |
| 31 | +def asm_check_stacktrace_leak(content: str) -> None: |
| 32 | + if not content: |
| 33 | + return |
| 34 | + |
| 35 | + try: |
| 36 | + # Quick check to avoid the slower operations if on stacktrace |
| 37 | + if "Traceback (most recent call last):" not in content: |
| 38 | + return |
| 39 | + |
| 40 | + text = HTML_TAGS_REMOVE.sub("", content) |
| 41 | + lines = [line.strip() for line in text.splitlines() if line.strip()] |
| 42 | + |
| 43 | + file_lines = [] |
| 44 | + exception_line = "" |
| 45 | + |
| 46 | + for i, line in enumerate(lines): |
| 47 | + if line.startswith("Traceback (most recent call last):"): |
| 48 | + # from here until we find an exception line |
| 49 | + continue |
| 50 | + |
| 51 | + # See if this line is a "File ..." line |
| 52 | + m_file = STACKTRACE_FILE_LINE.match(line) |
| 53 | + if m_file: |
| 54 | + file_lines.append(m_file.groups()) |
| 55 | + continue |
| 56 | + |
| 57 | + # See if this line might be the exception line |
| 58 | + m_exc = STACKTRACE_EXCEPTION_REGEX.match(line) |
| 59 | + if m_exc: |
| 60 | + # We consider it as the "final" exception line. Keep it. |
| 61 | + exception_line = m_exc.group("exc") |
| 62 | + # We won't break immediately because sometimes Django |
| 63 | + # HTML stack traces can have repeated exception lines, etc. |
| 64 | + # But typically the last match is the real final exception |
| 65 | + # We'll keep updating exception_line if we see multiple |
| 66 | + continue |
| 67 | + |
| 68 | + if not file_lines and not exception_line: |
| 69 | + return |
| 70 | + |
| 71 | + module_path = None |
| 72 | + if file_lines: |
| 73 | + # file_lines looks like [ ("/path/to/file.py", "line_no", "funcname"), ... ] |
| 74 | + last_file_entry = file_lines[-1] |
| 75 | + module_path = last_file_entry[0] # the path in quotes |
| 76 | + |
| 77 | + # Attempt to convert a path like "/myproj/foo/bar.py" into "foo.bar" |
| 78 | + # or "myproj.foo.bar" depending on your directory structure. |
| 79 | + # This is a *best effort* approach (it can be environment-specific). |
| 80 | + module_name = "" |
| 81 | + if module_path: |
| 82 | + mod_no_ext = re.sub(r"\.py$", "", module_path) |
| 83 | + parts: list[str] = [] |
| 84 | + while True: |
| 85 | + head, tail = os.path.split(mod_no_ext) |
| 86 | + if tail: |
| 87 | + parts.insert(0, tail) |
| 88 | + mod_no_ext = head |
| 89 | + else: |
| 90 | + # might still have a leftover 'head' if it’s not just root |
| 91 | + break |
| 92 | + |
| 93 | + module_name = ".".join(parts) |
| 94 | + if not module_name: |
| 95 | + module_name = module_path # fallback: just the path |
| 96 | + |
| 97 | + increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, StacktraceLeak.vulnerability_type) |
| 98 | + _set_metric_iast_executed_sink(StacktraceLeak.vulnerability_type) |
| 99 | + evidence = "Module: %s\nException: %s" % (module_name.strip(), exception_line.strip()) |
| 100 | + StacktraceLeak.report(evidence_value=evidence) |
| 101 | + except Exception as e: |
| 102 | + iast_taint_log_error("[IAST] error in check stacktrace leak. {}".format(e)) |
0 commit comments