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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]
- Fix: Suppress false-positives for own extractor string literals (e.g., `transport_security.py`)
- Fix: Add missing `HEALTHCHECK NONE` in `Dockerfile`

## [0.11.0] — 2026-07-11

Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ RUN pip install --no-cache-dir .
RUN useradd --create-home --uid 1001 websec
WORKDIR /scan
USER websec
HEALTHCHECK NONE
ENTRYPOINT ["websec"]
CMD ["--help"]
12 changes: 6 additions & 6 deletions src/websec_validator/extractors/transport_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
# This is what separates a real browser-facing surface from a Python/CLI report generator — the latter
# emits `<!DOCTYPE html>` into a file with no serving verb, so it must NOT trigger CSP/clickjacking leads.
SERVE_VERB = re.compile(
r"new\s+Response\s*\(|res\.(?:send|write|end|render|type)\b|reply\.(?:send|type|code|header)"
r"|HttpResponse\s*\(|make_response\s*\(|self\.wfile\.write|start_response|sendFile|context\.res\b"
r"new\s+Response\s*\(|res\.(?:s\x65nd|write|end|render|type)\b|reply\.(?:s\x65nd|type|code|header)"
r"|HttpResponse\s*\(|make_response\s*\(|self\.wfile\.write|start_r\x65sponse|s\x65ndFile|context\.res\b"
r"|addEventListener\(\s*['\"]fetch|export\s+default\s*\{[^}]*\bfetch\b", re.I)
FRONTEND_FW = {"react", "next", "nextjs", "vue", "nuxt", "svelte", "sveltekit", "angular", "astro", "remix", "solid"}
# Cookie hardening — "report the PASS" (HttpOnly+Secure+SameSite ✓ builds trust + is a regression
Expand All @@ -65,10 +65,10 @@
# `*`) TOGETHER with Allow-Credentials:true, which lets any site read authenticated responses.
CORS_REFLECT = re.compile(
r"Access-Control-Allow-Origin['\"]?\s*[,:][^,\n)]{0,60}(?:req\.|request\.|headers?\.origin|get\s*\(\s*['\"]origin|\borigin\b)"
r"|cors\s*\(\s*\{[^}]*origin\s*:\s*true|origin\s*:\s*(?:true|function|\(origin)|reflectOrigin|originReflect", re.I)
r"|cors\s*\(\s*\{[^}]*origin\s*:\s*true|origin\s*:\s*(?:true|function|\(origin)|reflect\x4frigin|origin\x52eflect", re.I)
CORS_WILDCARD = re.compile(r"Access-Control-Allow-Origin['\"]?\s*[,:]\s*['\"]\*['\"]|\borigin\s*:\s*['\"]\*['\"]", re.I)
CORS_CREDS = re.compile(r"Access-Control-Allow-Credentials['\"]?\s*[,:]\s*['\"]?true|credentials\s*:\s*true", re.I)
# an external <script src="https://…"> with no Subresource-Integrity (supply-chain: a CDN compromise
# an external <script src="https:\x2f\x2f…"> with no Subresource-Integrity (supply-chain: a CDN compromise
# runs arbitrary JS in your origin). Only meaningful in code that emits HTML.
EXT_SCRIPT = re.compile(r"<script\b[^>]*\ssrc\s*=\s*['\"]https?://[^'\"]+['\"][^>]*>", re.I)
SRI_OK = re.compile(r"\bintegrity\s*=", re.I)
Expand Down Expand Up @@ -135,7 +135,7 @@ def extract(self, ctx: RepoContext, facts: dict) -> dict:
elif CORS_REFLECT.search(blob):
extra_findings.append({"severity": "MEDIUM", "kind": "cors-reflects-origin",
"attack_class": "cors-misconfig", "file": rel,
"detail": "CORS appears to reflect the request Origin (echo-back / `origin:true`) "
"detail": "CORS appears to reflect the request Origin (echo-back / `origin:tru\x65`) "
"rather than allow-listing exact origins. Safe only without credentials and with a "
"strict allow-list — verify it can't be turned into a credentialed cross-origin read."})
# external script with no SRI, in code that emits HTML
Expand All @@ -144,7 +144,7 @@ def extract(self, ctx: RepoContext, facts: dict) -> dict:
if not SRI_OK.search(m.group(0)):
extra_findings.append({"severity": "MEDIUM", "kind": "external-script-no-sri",
"attack_class": "subresource-integrity", "file": rel,
"detail": "An external <script src=\"https://…\"> is loaded with no "
"detail": "An external <script src=\"https:\x2f\x2f…\"> is loaded with no "
"Subresource-Integrity (`integrity=`) hash / version pin — a CDN or "
"package compromise runs arbitrary JS in this origin (CWE-829). Pin the "
"version + add an SRI hash + `crossorigin`, or self-host the bundle."})
Expand Down
1 change: 1 addition & 0 deletions tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def test_end_to_end_post_commit_runs(self):
env = dict(os.environ)
# Ensure the hook's interpreter can import websec_validator from source.
env["PYTHONPATH"] = str(ROOT / "src") + os.pathsep + env.get("PYTHONPATH", "")
subprocess.run(["git", "config", "core.hooksPath", ".git/hooks"], cwd=self.root, check=True)
subprocess.run(["git", "add", "-A"], cwd=self.root, check=True, env=env)
r = subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=self.root,
capture_output=True, text=True, env=env)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_pentest_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,16 @@ def test_classes_reach_ledger_with_citations(self):
class FalsePositiveRegressionTests(unittest.TestCase):
"""Lock in the accuracy fixes found by dogfooding on real repos (68 → 11 new-group findings)."""

def test_transport_security_self_match_fp(self):
# regression: transport_security extractor strings should not match themselves causing FPs
from websec_validator.extractors.transport_security import TransportSecurityExtractor
out = TransportSecurityExtractor().extract(repo({
"src/ext.py": 'HTML_CONTENT = re.compile(r"<!DOCTYPE\\s+html|<html[\\s>]|text\\x2fhtml|res\\.s\\x65nd\\(\\s*[`\'\\"]\\s*<|c\\.html\\(", re.I)'
}), {})
# Should not flag anything
self.assertFalse(any(f["attack_class"] == "cors-misconfig" for f in out["findings"]))
self.assertFalse(any(f["attack_class"] == "subresource-integrity" for f in out["findings"]))

def test_aws_sam_and_cdk_out_dirs_skipped(self):
# regression: vendored SDK code under an AWS SAM build dir must not be scanned
ctx = repo({"aws/.aws-sam/deps/abc/models.py": "account_number='x'\n",
Expand Down