Skip to content

Security Considerations

Mikhail Deynekin edited this page Oct 14, 2026 · 1 revision

Security Considerations

This page covers two distinct subjects: how to use the kit safely (its probe batteries fire real attack payloads), and the kit's own security posture (execution surface, supply chain, publication safety). Every statement below is traced to a source line in commit 7e17a11 or to a test executed during this wiki run (test IDs like T14 refer to the logs in research/evidence/ of the run workspace).

Contents


Threat model

Important

The probe stack sends path-traversal, XSS, SSTI, command-injection, SQLi and CRLF payloads at whatever URL you point it at. Run batteries only against software you own or are explicitly authorized to test — in practice, against the throwaway staged instance that lib/serve.py creates, never against a production host or a third party's site.

The kit touches security in five places:

# Surface What can go wrong Mitigation built into the kit
1 Probe batteries (probe/webapp_probe.py) Attack payloads hitting an unauthorized target Batteries run against a ServedApp staged copy on 127.0.0.1; --base-url exists for live targets and is the operator's responsibility
2 Staged instances (lib/serve.py) Port collisions serving stale state; leftover processes Verified-free port (raises if a preferred port is busy, lib/serve.py:58), temp docroot, killpg on exit (lib/serve.py:176)
3 Executing subject code The subject is untrusted code running in your sandbox Staging copies only the files you list; the run happens in a temp dir; PITFALLS B10 refuses to probe when the runtime cannot boot the subject (setup/check_runtime.py, exit 2)
4 Evidence files Probe JSON/MD capture response excerpts that may contain sensitive data Excerpts are truncated to 180 chars (probe/webapp_probe.py:91); .gitignore blocks run artefacts (workspace/, *.bundle, uploads/, v3.*/, workspace-fallback/) from being committed into the kit
5 Publishing (publish/publish.sh) Destroying an owner's wiki history; leaking credentials Never prompts, never force-pushes, QA-gates before committing, and produces a bundle instead of requiring credentials

The payload inventories

The batteries ship two fixed payload lists. They are generic, well-known test vectors — the same class of payloads a documentation author needs in order to state, with evidence, whether an escaping or path-validation layer actually holds.

Path traversal — 20 payloads (probe/webapp_probe.py:65)

Category Examples from the list Count
Classic relative/absolute ../../../etc/passwd, /etc/passwd, C:\windows\win.ini 5
URL-encoded ..%2f..%2f..%2fetc%2fpasswd, %2e%2e%2f%2e%2e%2fetc%2fpasswd 2
Double-encoded ..%252f..%252fetc%252fpasswd 1
UTF-8 overlong %c0%ae%c0%ae/etc/passwd, ..%c0%af..%c0%afetc/passwd 2
Filter-evasion ....//....//....//etc/passwd, valid/../../../etc/passwd, ./../../etc/passwd 3
NUL byte ..%00/..%00/etc/passwd, etc/passwd%00.png, \x00 3
Sensitive procfs /proc/self/environ 1
Overlong paths 40×.., 60×a/, 300-char tail 3

Classification is deliberately body-based, not status-based: a refusal may legitimately return HTTP 200 with a denial banner (deliberate non-disclosure), so battery_traversal matches deny_markers in the body and separately checks leak markers (root:x:0:0, [boot loader], DB_PASSWORD, BEGIN RSA PRIVATE KEY) — probe/webapp_probe.py:171.

Injection — 26 payloads (probe/webapp_probe.py:79)

Category Example Count
Reflected/DOM XSS <script>alert(1)</script>, <img src=x onerror=alert(1)>, <svg onload=alert(1)>, <iframe src="javascript:alert(1)">, <details open ontoggle=…>, <body onload=…>, <input onfocus=… autofocus> 7
Event-handler/attribute breakout <a href="x" onclick="alert(1)">y</a>, "'><script>… 2
DOM clobbering <form id=x><input name=y> 1
URI scheme javascript:alert(1) 1
SSTI {{7*7}}, ${7*7}, <%= 7*7 %>, #{7*7} 4
Command injection $(id), backtick-id, ;id;, pipe-id 4
SQLi ' OR '1'='1, 1; DROP TABLE users-- 2
CRLF/header injection %0d%0aX-Injected: yes, a\r\nb 2
HTML oddities <!-- comment -->, <plaintext>, <noscript>x 3

classify_injection (probe/webapp_probe.py:97) returns one verdict per payload: REFLECTED-VERBATIM (the finding that matters), escaped, reflected-case-changed, TAG-SURVIVED, SSTI-49, empty-body, not-reflected.

Note

These batteries make the kit a documentation-evidence tool, not a substitute for a professional penetration test. There is no coverage guarantee: 26 payloads is a probe of whether escaping exists at all, not a fuzzing campaign.

Case study: what a battery actually produced (test T14)

Run against the reference subject MD.Viewer 2.18.0 (staged copy, PHP 8.2.34, full php-mdviewer suite in 0.46 s of wall time):

Battery Measured result
Traversal (20 payloads) 17 denied by body marker; 2 answered 301 (harmless normalisation); 1 NUL-byte payload failed at transport level (ERR); 0 leaked sensitive content
Injection (26 payloads) 19 survived verbatim into the response — raw-HTML passthrough confirmed, matching the figure the kit's README documents from the original run
Security headers /md.php missing CSP, X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy, Permissions-Policy
Leak headers X-Powered-By: PHP/8.2.34 present
Upload matrix (16 filenames) bad.php → 400 "Only .md files are allowed."; .hidden.md → 400 forbidden pattern; ../evil.md → 200 stored as evil.md (basename sanitisation); unicode filenames accepted

This is the kit's verification model at work: the wiki for MD.Viewer could state its security behaviour with reproduced evidence instead of transcribed claims. The findings belong to the subject, not to the kit — keep those two separated when reading examples/.

The kit's own execution surface

Where the tools spawn processes or open sockets (all verified by reading the code at 7e17a11):

Tool exec/network behaviour Risk assessment
lib/serve.py subprocess.Popen(cmd.split(), preexec_fn=os.setsid); serves on 127.0.0.1 only (default host); kills the process group on exit The serve command comes from the operator (--serve-cmd), not from the subject. No shell interpolation
lib/req.py, lib/session.py stdlib urllib against operator-supplied URLs; no redirects by default No listener; requests only where you point them
qa/qa_wiki.py bash -n with the block passed via input= (not as an argument); compile() for Python blocks — never executes wiki code Syntax checking without execution; a malicious shell block in a wiki cannot run through the auditor
setup/check_runtime.py shell=True on fixed command strings (php -r …, node -v) No untrusted input reaches the shell
qa/verify_commands.py run() with shell=True for documented commands — by design it executes what the wiki says Template-shaped harness; you review the command list before running it
setup/bootstrap.sh apt-get update + installs; optionally adds the Sury APT repository (PHP 8.3 recipe) Installs distro packages; the Sury path adds a third-party signing key — review before enabling --with-php83
publish/publish.sh git clone https://github.com/OWNER/REPO.wiki.git; no credentials used or stored Egress limited to github.com; fails safely into the bundle path

The kit makes no LLM API calls — there is no network client in any tool other than the HTTP probe helpers and git/apt in the setup and publish scripts. The full external-host inventory extracted by scan/repo_scan.py from the subject tree confirms this: runtime hosts are github.com, raw.githubusercontent.com, api.github.com (bootstrap egress check), packages.sury.org (recipe text) and distro apt mirrors; every other host found by the scan (img.shields.io, cdn.jsdelivr.net, fonts.googleapis.com, cheatsheetseries.owasp.org) appears only inside examples/sample-pages/ — delivered-wiki samples, not code the tools execute.

Tip

The scaffolded Home.md template embeds a shields.io badge image (scaffold/new_page.py, HOME_TMPL). If your wiki must not reference third-party hosts, delete the badge line before publishing; the QA tool does not require it.

Data you might leak, and where it lands

  1. Evidence files (research/probe.json, *.md) contain request logs, response excerpts and header dumps from the subject. If the subject echoes secrets in responses (session tokens, API keys in bodies), they land in your evidence. Treat research/ as sensitive until reviewed.
  2. The staged docroot lives in a temp dir (probe-app-*) and is deleted on exit unless you pass --keep; with --keep, review what the subject wrote to disk (battery_disk lists it) before archiving.
  3. The delivered wiki is public (for public repos). Never paste API keys, customer data or unpatched-exploit detail into pages. PROMPT.md has a SENSITIVE: header field exactly for declaring what must not be published.
  4. publish/ output contains a full clone plus a git bundle of the wiki; both are as public as the wiki itself.

The .gitignore in the kit defends the other direction — a real near-miss recorded in CHANGELOG.md 1.2.1: a 184-file, 4.4 MB run workspace (v3.2/, containing a subject's uploads and bundles) sat untracked-but-not-ignored at the repository root, one git add -A away from being published. CI now refuses to pass when such a directory is present (.github/workflows/selftest.yml, workspace-refusal step).

Publication safety

publish/publish.sh encodes four rules (all exercised in test T15):

  1. QA gate first — qa_wiki.py --strict must exit 0 before anything is copied; a wiki with dead links is not published.
  2. Never force-push — the .wiki.git repository may contain the owner's commits (GitHub creates an Initial commit when the wiki is initialised through the UI). The script clones, commits on top, and hands over.
  3. No credential requirement — the artefacts are a prepared clone with origin configured, a verified bundle (git bundle verify output printed), and PUBLISH.md with three push options. The push itself happens where credentials exist.
  4. Self-describing hand-over — PUBLISH.md records branch (master), commit SHA, remote URL, bundle path and the B8 warning that cloning a bundle directly needs git clone -b master … (a bundle carries no HEAD symref; without -b you get an empty tree and every later check passes vacuously).

Hardening checklist for a run

  • Read setup/PITFALLS.md before writing any probe — its first three entries produce silently wrong results.
  • Run bash setup/bootstrap.sh --subject <dir> so the runtime-floor gate can refuse a subject the interpreter cannot boot (exit 2).
  • Probe staged copies (--repo), not live third-party hosts (--base-url only with written authorization).
  • Review research/evidence/ for secrets before committing anything anywhere.
  • Keep run artefacts inside workspace/ — the kit's .gitignore covers that path; a stray copy at the repo root is the 1.2.1 near-miss.
  • Snapshot at phase boundaries (setup/snapshot.sh workspace "phase N done") so a bad bulk edit costs minutes.
  • Before publishing, re-read pages that quote error strings, keys or paths verbatim — verbatim subject strings win over terminology rules, but they still must not contain live secrets.

Known limitations (honest section)

  • No SECURITY.md and no signed-release process exist in the subject repository at 7e17a11 (verified: file absent, zero tags). Report issues through the repository's issue tracker; there is no vulnerability-disclosure channel documented.
  • Payload lists are static. A subject is not "secure" because 20 traversal payloads were denied; the lists detect absent defence, they do not prove present defence against everything.
  • qa_wiki.py checks shell blocks with bash -n (syntax only) and Python blocks with compile() (parse only). A syntactically valid but destructive command passes the auditor — the re-run step of Phase 4 (in a clean sandbox) is where documented commands are actually executed, and you decide what is safe to run.
  • The auditor's ban/terms matching is substring-based on a scrubbed view with a negation heuristic; it is a discipline aid, not a proof of factual accuracy.

See also

Clone this wiki locally