diff --git a/app/src/App.php b/app/src/App.php
index c602450..7302a8d 100644
--- a/app/src/App.php
+++ b/app/src/App.php
@@ -24,6 +24,14 @@ class App
self::$booted = true;
}
if (session_status() === PHP_SESSION_NONE) {
+ $isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
+ session_set_cookie_params([
+ 'lifetime' => 0,
+ 'path' => '/',
+ 'secure' => $isSecure,
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ ]);
session_start();
}
self::ensureCsrf();
diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php
index 005bae2..bb306b6 100644
--- a/app/templates/admin/acces.php
+++ b/app/templates/admin/acces.php
@@ -1442,6 +1442,19 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: srupvzpw 5a2ec7e2 "feat: add passive pentest scanner script with PEP 723 uv metadata" (rebased revision)
++ $linkName = $link['name'] ?? '';
+++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: srupvzpw 5a2ec7e2 "feat: add passive pentest scanner script with PEP 723 uv metadata" (rebased revision)
+\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+- $linkName = $link['name'] ?? '';
+- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
+\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: npqqxmut 9daffcda "fix: harden security based on pentest scan findings" (rebased revision)
+ $linkName = $link['name'] ?? '';
+ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
+ $linkLockedYear = $link['locked_year'] ?? null;
++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
++\\\\\\\ to: npqqxmut d463ff53 "fix: harden security based on pentest scan findings" (rebased revision)
+++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?>
diff --git a/justfile b/justfile
index c86279a..95aa955 100644
--- a/justfile
+++ b/justfile
@@ -60,9 +60,9 @@ deploy:
--exclude 'storage/docs' \
--exclude 'var/' \
app/ xamxam:/var/www/xamxam/
- # Upload deploy-server.sh for post-deploy permission fix
- rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
+ # Deploy nginx config + fix permissions + reload (single server-side run)
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
+ rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh -t xamxam "sudo bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf"
ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}"
@@ -70,8 +70,6 @@ deploy:
rsync -v scripts/migrate.sh xamxam:/tmp/migrate.sh
ssh xamxam "cd /var/www/xamxam && REPO_ROOT=/var/www/xamxam bash /tmp/migrate.sh"
ssh xamxam "rm -f /tmp/migrate.sh"
- # Deploy nginx configuration
- @just deploy-nginx
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
@just deploy-env
@just deploy-verify-permissions
diff --git a/nginx/docs/SECURITY_HEADERS.md b/nginx/docs/SECURITY_HEADERS.md
index 81802d4..d80be40 100644
--- a/nginx/docs/SECURITY_HEADERS.md
+++ b/nginx/docs/SECURITY_HEADERS.md
@@ -4,16 +4,20 @@
| Header | Value | Purpose |
|--------|-------|---------|
+| `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload;` | HSTS — forces HTTPS |
+| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding |
| `X-Frame-Options` | `SAMEORIGIN` | Prevent clickjacking |
| `X-Content-Type-Options` | `nosniff` | Prevent MIME-type sniffing |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage |
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | Disable unused browser APIs |
+| `Cross-Origin-Opener-Policy` | `same-origin` | Isolates browsing context |
+| `Cross-Origin-Resource-Policy` | `same-origin` | Controls cross-origin resource sharing |
-## Headers in use (`/admin/` location block)
+## Headers in use (`/admin/` location block — inherited from main + overrides)
| Header | Value | Purpose |
|--------|-------|---------|
-| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding |
+| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor |
| `X-Robots-Tag` | `noindex, nofollow` | Prevent search-engine indexing of admin |
These were previously declared in `public/admin/.htaccess` as Apache
@@ -36,8 +40,3 @@ no protection and may introduce risk.
**Correct mitigation:** a proper `Content-Security-Policy` header (now done for
`/admin/`; public-page CSP is todo item #11).
-## Pending headers
-
-| Header | Scope | Status |
-|--------|-------|--------|
-| `Content-Security-Policy` | Public pages (non-admin) | ⏳ todo item #11 |
diff --git a/nginx/xamxam.conf b/nginx/xamxam.conf
index fa771fe..6aa2ce4 100644
--- a/nginx/xamxam.conf
+++ b/nginx/xamxam.conf
@@ -36,12 +36,14 @@ server {
index index.php index.html index.htm;
# Security headers
+ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;" always;
+ add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
- # X-XSS-Protection intentionally omitted — deprecated and counterproductive in modern browsers.
- # CSP (Content-Security-Policy) is the correct mitigation.
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
+ add_header Cross-Origin-Opener-Policy "same-origin" always;
+ add_header Cross-Origin-Resource-Policy "same-origin" always;
# Server tokens already disabled in nginx.conf
# server_tokens off;
diff --git a/nginx/xamxam.conf.reference b/nginx/xamxam.conf.reference
index e2b874d..e3e77b6 100644
--- a/nginx/xamxam.conf.reference
+++ b/nginx/xamxam.conf.reference
@@ -34,12 +34,14 @@ server {
index index.php index.html index.htm;
# Security headers
+ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;" always;
+ add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
- # X-XSS-Protection intentionally omitted — deprecated and counterproductive in modern browsers.
- # CSP (Content-Security-Policy) is the correct mitigation.
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
+ add_header Cross-Origin-Opener-Policy "same-origin" always;
+ add_header Cross-Origin-Resource-Policy "same-origin" always;
# Server tokens already disabled in nginx.conf
# server_tokens off;
diff --git a/pentest_xamxam.py b/pentest_xamxam.py
deleted file mode 100644
index 5d0a208..0000000
--- a/pentest_xamxam.py
+++ /dev/null
@@ -1,469 +0,0 @@
-#!/usr/bin/env -S uv run
-# /// script
-# requires-python = ">=3.12"
-# dependencies = [
-# "requests",
-# "dnspython",
-# "colorama",
-# ]
-# ///
-"""
-Passive / Non-Destructive Pentest Script
-Target: xamxam.erg.be
-Author: generated for site owner / authorized use only
-
-Checks performed:
- 1. DNS & WHOIS metadata
- 2. TLS certificate info
- 3. HTTP security headers audit
- 4. Cookie flags audit
- 5. Sensitive file/path exposure
- 6. robots.txt & sitemap.xml parsing
- 7. Open redirect probe (non-exploiting)
- 8. Reflected XSS canary in search param (detection only)
- 9. Directory listing detection
- 10. Software version disclosure in headers/meta
- 11. CORS misconfiguration check
- 12. Clickjacking (X-Frame-Options / CSP frame-ancestors)
- 13. HSTS check
- 14. Subdomain enumeration (wordlist)
- 15. Summary report
-
-Usage:
- pip install requests dnspython colorama
- python3 pentest_xamxam.py
-"""
-
-import socket
-import ssl
-import json
-import re
-import datetime
-import urllib.parse
-from dataclasses import dataclass, field
-from typing import Optional
-
-import requests
-import dns.resolver
-from colorama import Fore, Style, init
-
-init(autoreset=True)
-
-# ── Config ────────────────────────────────────────────────────────────────────
-TARGET_HOST = "xamxam.erg.be"
-TARGET_BASE = f"https://{TARGET_HOST}"
-TIMEOUT = 8
-HEADERS = {"User-Agent": "PentestScanner/1.0 (authorized audit)"}
-
-# Paths an attacker would probe first
-SENSITIVE_PATHS = [
- "/.env", "/.env.local", "/.env.production",
- "/.git/HEAD", "/.git/config",
- "/wp-admin", "/wp-login.php", # CMS probes
- "/admin", "/administrator", "/admin/login",
- "/phpmyadmin", "/pma",
- "/backup.zip", "/backup.sql", "/db.sql",
- "/config.php", "/config.json", "/settings.py",
- "/robots.txt", "/sitemap.xml",
- "/.htaccess", "/web.config",
- "/api", "/api/v1", "/api/users",
- "/graphql",
- "/server-status", # Apache status page
- "/actuator", "/actuator/health", # Spring Boot
- "/_next/static", # Next.js artefacts
- "/static",
- "/uploads", "/files", "/media",
-]
-
-SECURITY_HEADERS = {
- "Strict-Transport-Security": "HSTS — forces HTTPS",
- "Content-Security-Policy": "CSP — mitigates XSS",
- "X-Frame-Options": "Clickjacking protection",
- "X-Content-Type-Options": "MIME sniffing protection",
- "Referrer-Policy": "Controls referrer leakage",
- "Permissions-Policy": "Limits browser feature access",
- "X-XSS-Protection": "Legacy XSS filter (deprecated but informative)",
- "Cross-Origin-Opener-Policy": "Isolates browsing context",
- "Cross-Origin-Resource-Policy": "Controls cross-origin resource sharing",
-}
-
-SUBDOMAIN_WORDLIST = [
- "www", "mail", "smtp", "ftp", "dev", "staging", "test", "api",
- "admin", "portal", "old", "beta", "cdn", "static", "media",
- "docs", "blog", "intranet", "vpn", "login", "auth",
-]
-
-XSS_CANARY = ""
-OPEN_REDIRECT_PAYLOADS = ["//evil.com", "https://evil.com", "/\\evil.com"]
-
-
-# ── Helpers ───────────────────────────────────────────────────────────────────
-def ok(msg): print(f" {Fore.GREEN}[✓]{Style.RESET_ALL} {msg}")
-def warn(msg): print(f" {Fore.YELLOW}[!]{Style.RESET_ALL} {msg}")
-def bad(msg): print(f" {Fore.RED}[✗]{Style.RESET_ALL} {msg}")
-def info(msg): print(f" {Fore.CYAN}[i]{Style.RESET_ALL} {msg}")
-def section(title):
- print(f"\n{Fore.MAGENTA}{'═'*60}{Style.RESET_ALL}")
- print(f"{Fore.MAGENTA} {title}{Style.RESET_ALL}")
- print(f"{Fore.MAGENTA}{'═'*60}{Style.RESET_ALL}")
-
-
-@dataclass
-class Finding:
- severity: str # CRITICAL / HIGH / MEDIUM / LOW / INFO
- title: str
- detail: str
-
-
-findings: list[Finding] = []
-
-def add(severity, title, detail=""):
- findings.append(Finding(severity, title, detail))
-
-
-def get(path="", params=None, allow_redirects=True) -> Optional[requests.Response]:
- try:
- return requests.get(
- TARGET_BASE + path,
- params=params,
- headers=HEADERS,
- timeout=TIMEOUT,
- allow_redirects=allow_redirects,
- verify=True,
- )
- except requests.exceptions.SSLError as e:
- add("HIGH", f"SSL error on {path}", str(e))
- return None
- except requests.exceptions.ConnectionError:
- return None
- except requests.exceptions.Timeout:
- return None
-
-
-# ── 1. DNS & IP ───────────────────────────────────────────────────────────────
-def check_dns():
- section("1. DNS & IP Resolution")
- try:
- ip = socket.gethostbyname(TARGET_HOST)
- info(f"A record → {ip}")
- add("INFO", "A record", ip)
- except Exception as e:
- bad(f"DNS resolution failed: {e}")
-
- for rtype in ("MX", "TXT", "NS", "AAAA"):
- try:
- answers = dns.resolver.resolve(TARGET_HOST, rtype)
- for r in answers:
- info(f"{rtype:5s} → {r}")
- except Exception:
- pass
-
-
-# ── 2. TLS Certificate ────────────────────────────────────────────────────────
-def check_tls():
- section("2. TLS Certificate")
- try:
- ctx = ssl.create_default_context()
- with ctx.wrap_socket(socket.socket(), server_hostname=TARGET_HOST) as s:
- s.connect((TARGET_HOST, 443))
- cert = s.getpeercert()
-
- subject = dict(x[0] for x in cert["subject"])
- issuer = dict(x[0] for x in cert["issuer"])
- not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
- days_left = (not_after.replace(tzinfo=datetime.timezone.utc) - datetime.datetime.now(datetime.UTC)).days
-
- info(f"Subject : {subject.get('commonName')}")
- info(f"Issuer : {issuer.get('organizationName')}")
- info(f"Expires : {not_after.date()} ({days_left} days)")
-
- sans = [v for t, v in cert.get("subjectAltName", []) if t == "DNS"]
- info(f"SANs : {', '.join(sans)}")
-
- if days_left < 30:
- bad(f"Certificate expires in {days_left} days!")
- add("HIGH", "Certificate near expiry", f"{days_left} days remaining")
- else:
- ok("Certificate valid and not near expiry")
-
- except Exception as e:
- bad(f"TLS check failed: {e}")
- add("HIGH", "TLS check failed", str(e))
-
-
-# ── 3. Security Headers ───────────────────────────────────────────────────────
-def check_headers():
- section("3. HTTP Security Headers")
- r = get("/")
- if not r:
- bad("Could not reach target"); return
-
- # Show server / technology leakage
- for leak_header in ("Server", "X-Powered-By", "X-AspNet-Version", "X-Generator"):
- val = r.headers.get(leak_header)
- if val:
- warn(f"Version disclosure — {leak_header}: {val}")
- add("LOW", f"Version disclosure: {leak_header}", val)
-
- for header, description in SECURITY_HEADERS.items():
- val = r.headers.get(header)
- if val:
- ok(f"{header}: {val}")
- else:
- bad(f"Missing: {header} ({description})")
- severity = "HIGH" if header in ("Strict-Transport-Security", "Content-Security-Policy", "X-Frame-Options") else "MEDIUM"
- add(severity, f"Missing header: {header}", description)
-
-
-# ── 4. Cookie Flags ───────────────────────────────────────────────────────────
-def check_cookies():
- section("4. Cookie Security Flags")
- r = get("/")
- if not r: return
-
- raw_cookies = r.headers.get("Set-Cookie", "")
- if not raw_cookies:
- info("No Set-Cookie header on homepage (may appear after login)")
- return
-
- for cookie_str in raw_cookies.split(","):
- name = cookie_str.split("=")[0].strip()
- flags = cookie_str.lower()
- issues = []
- if "httponly" not in flags:
- issues.append("missing HttpOnly")
- if "secure" not in flags:
- issues.append("missing Secure")
- if "samesite" not in flags:
- issues.append("missing SameSite")
- if issues:
- warn(f"Cookie '{name}': {', '.join(issues)}")
- add("MEDIUM", f"Cookie flags: {name}", "; ".join(issues))
- else:
- ok(f"Cookie '{name}' has all recommended flags")
-
-
-# ── 5. Sensitive File Exposure ────────────────────────────────────────────────
-def check_sensitive_paths():
- section("5. Sensitive Path Exposure")
- for path in SENSITIVE_PATHS:
- r = get(path, allow_redirects=False)
- if r is None:
- continue
- if r.status_code == 200:
- size = len(r.content)
- if path in ("/robots.txt", "/sitemap.xml"):
- info(f"{r.status_code} {path} ({size}B)")
- add("INFO", f"Found: {path}", r.text[:300])
- else:
- bad(f"{r.status_code} {path} ({size}B) ← EXPOSED")
- add("HIGH", f"Sensitive path exposed: {path}", f"HTTP 200, {size} bytes")
- elif r.status_code in (301, 302):
- loc = r.headers.get("Location", "")
- info(f"{r.status_code} {path} → {loc}")
- elif r.status_code == 403:
- warn(f"403 {path} (exists but forbidden — still interesting)")
- add("LOW", f"Forbidden path (exists): {path}")
-
-
-# ── 6. robots.txt & sitemap ───────────────────────────────────────────────────
-def check_robots():
- section("6. robots.txt Analysis")
- r = get("/robots.txt")
- if r and r.status_code == 200:
- info("robots.txt found:")
- disallowed = [l for l in r.text.splitlines() if "Disallow" in l]
- for line in disallowed:
- warn(f" {line.strip()} ← attacker will check these first")
- path = line.split(":", 1)[-1].strip()
- add("INFO", "robots.txt Disallow path", path)
- else:
- info("No robots.txt found")
-
-
-# ── 7. Open Redirect ─────────────────────────────────────────────────────────
-def check_open_redirect():
- section("7. Open Redirect Probe")
- redirect_params = ["redirect", "url", "next", "return", "returnUrl", "goto", "dest", "target"]
- for param in redirect_params:
- for payload in OPEN_REDIRECT_PAYLOADS[:1]: # Just one payload for non-destructive check
- r = get("/", params={param: payload}, allow_redirects=False)
- if r and r.status_code in (301, 302, 303, 307, 308):
- loc = r.headers.get("Location", "")
- if "evil.com" in loc:
- bad(f"Open redirect via ?{param}= → {loc}")
- add("HIGH", "Open Redirect", f"?{param}={payload} → {loc}")
- else:
- info(f"Redirect on ?{param}= but to: {loc} (not vulnerable)")
- # No redirect = not vulnerable to this param
-
-
-# ── 8. XSS Canary in Search ───────────────────────────────────────────────────
-def check_xss():
- section("8. Reflected XSS Canary (search param)")
- search_paths = ["/", "/repertoire", "/search"]
- search_params = ["q", "s", "query", "search", "keyword", "term"]
-
- for path in search_paths:
- for param in search_params:
- r = get(path, params={param: XSS_CANARY})
- if r and XSS_CANARY in r.text:
- bad(f"XSS canary reflected unencoded at {path}?{param}=")
- add("CRITICAL", "Reflected XSS", f"{path}?{param}={XSS_CANARY}")
- elif r and urllib.parse.quote(XSS_CANARY) in r.text:
- ok(f"{path}?{param}= — canary is URL-encoded (likely safe)")
- elif r and "<script>" in r.text.lower():
- ok(f"{path}?{param}= — canary is HTML-encoded (safe)")
-
-
-# ── 9. Directory Listing ──────────────────────────────────────────────────────
-def check_dir_listing():
- section("9. Directory Listing")
- dirs = ["/uploads/", "/files/", "/media/", "/static/", "/assets/"]
- for path in dirs:
- r = get(path)
- if r and r.status_code == 200:
- markers = ["index of", "parent directory", "directory"]
- if any(m in r.text.lower() for m in markers):
- bad(f"Directory listing ENABLED at {path}")
- add("HIGH", "Directory listing enabled", path)
- else:
- info(f"{path} → 200 but no listing detected")
-
-
-# ── 10. CORS Misconfiguration ─────────────────────────────────────────────────
-def check_cors():
- section("10. CORS Misconfiguration")
- try:
- r = requests.get(
- TARGET_BASE + "/api",
- headers={**HEADERS, "Origin": "https://evil.com"},
- timeout=TIMEOUT,
- verify=True,
- )
- acao = r.headers.get("Access-Control-Allow-Origin", "")
- acac = r.headers.get("Access-Control-Allow-Credentials", "")
- if acao == "*":
- warn("CORS: Access-Control-Allow-Origin: * (wildcard — no credentials risk)")
- add("LOW", "CORS wildcard origin", "ACAO: *")
- elif acao == "https://evil.com":
- bad("CORS reflects arbitrary Origin!")
- add("HIGH", "CORS reflects arbitrary Origin", f"ACAO: {acao}, ACAC: {acac}")
- if acac.lower() == "true":
- add("CRITICAL", "CORS reflects Origin + credentials allowed", "Full CORS bypass")
- else:
- ok(f"CORS origin: '{acao}' (not reflected)")
- except Exception as e:
- info(f"CORS check skipped: {e}")
-
-
-# ── 11. Subdomain Enumeration ─────────────────────────────────────────────────
-def check_subdomains():
- section("11. Subdomain Enumeration (passive wordlist)")
- parent = ".".join(TARGET_HOST.split(".")[-2:]) # erg.be
- found = []
- for sub in SUBDOMAIN_WORDLIST:
- fqdn = f"{sub}.{parent}"
- try:
- ip = socket.gethostbyname(fqdn)
- info(f"Found: {fqdn} → {ip}")
- found.append(fqdn)
- add("INFO", "Subdomain found", f"{fqdn} → {ip}")
- except socket.gaierror:
- pass
- if not found:
- ok("No additional subdomains discovered from wordlist")
-
-
-# ── 12. HSTS Preload ──────────────────────────────────────────────────────────
-def check_hsts():
- section("12. HSTS & HTTPS Redirect")
- # Check HTTP → HTTPS redirect
- try:
- r = requests.get(f"http://{TARGET_HOST}/", headers=HEADERS, timeout=TIMEOUT,
- allow_redirects=False, verify=False)
- if r.status_code in (301, 302):
- loc = r.headers.get("Location", "")
- if loc.startswith("https://"):
- ok(f"HTTP redirects to HTTPS ({r.status_code})")
- else:
- bad(f"HTTP redirects to non-HTTPS: {loc}")
- add("HIGH", "HTTP does not redirect to HTTPS", loc)
- else:
- bad(f"HTTP returns {r.status_code} (no HTTPS redirect)")
- add("HIGH", "No HTTP→HTTPS redirect", f"Status: {r.status_code}")
- except Exception:
- pass
-
- r = get("/")
- if r:
- hsts = r.headers.get("Strict-Transport-Security", "")
- if "max-age" in hsts:
- age = re.search(r"max-age=(\d+)", hsts)
- age_val = int(age.group(1)) if age else 0
- if age_val < 31536000:
- warn(f"HSTS max-age too short: {age_val}s (recommend ≥31536000)")
- add("LOW", "HSTS max-age too short", f"{age_val}s")
- else:
- ok(f"HSTS max-age OK: {age_val}s")
- if "includeSubDomains" not in hsts:
- warn("HSTS missing includeSubDomains")
- if "preload" not in hsts:
- warn("HSTS missing preload directive")
- else:
- bad("HSTS header missing entirely")
- add("HIGH", "HSTS not set")
-
-
-# ── Summary ───────────────────────────────────────────────────────────────────
-def print_summary():
- section("SUMMARY REPORT")
- order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
- color_map = {
- "CRITICAL": Fore.RED,
- "HIGH": Fore.YELLOW,
- "MEDIUM": Fore.CYAN,
- "LOW": Fore.WHITE,
- "INFO": Fore.BLUE,
- }
- sorted_findings = sorted(findings, key=lambda f: order.get(f.severity, 9))
- counts = {k: 0 for k in order}
- for f in sorted_findings:
- counts[f.severity] = counts.get(f.severity, 0) + 1
- c = color_map.get(f.severity, "")
- print(f" {c}[{f.severity:8s}]{Style.RESET_ALL} {f.title}")
- if f.detail:
- detail_preview = f.detail[:120].replace("\n", " ")
- print(f" └─ {detail_preview}")
-
- print()
- for sev, cnt in counts.items():
- if cnt:
- c = color_map[sev]
- print(f" {c}{sev}: {cnt}{Style.RESET_ALL}")
-
- print(f"\n Scan completed: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
- print(f" Target: {TARGET_BASE}")
-
-
-# ── Main ──────────────────────────────────────────────────────────────────────
-if __name__ == "__main__":
- print(f"""
-{Fore.CYAN}╔══════════════════════════════════════════════════════════╗
-║ Passive Pentest Scanner — authorized use only ║
-║ Target : {TARGET_HOST:<44} ║
-╚══════════════════════════════════════════════════════════╝{Style.RESET_ALL}
-""")
- check_dns()
- check_tls()
- check_headers()
- check_cookies()
- check_sensitive_paths()
- check_robots()
- check_open_redirect()
- check_xss()
- check_dir_listing()
- check_cors()
- check_subdomains()
- check_hsts()
- print_summary()