# webapp-probe

Probe a web app for what it exposes or leaks - passively from captured traffic and actively against the target. Use to assess a web app's exposure or review Burp/ZAP history.

- **Kind:** skill
- **Source:** https://github.com/forefy/.context
- **Page:** https://forefy.com/skills/de6bd2de-73f9-4f1c-87c2-d637e2e827ec
- **API (JSON + files):** https://forefy.com/api/asr/de6bd2de-73f9-4f1c-87c2-d637e2e827ec

---

## SKILL.md

---
name: webapp-probe
description: Probe a web app for what it exposes or leaks - passively from captured traffic and actively against the target. Use to assess a web app's exposure or review Burp/ZAP history.
---

## Contents
- Scope & authorization (blast-radius labels)
- Passive analysis of captured traffic:
  1. Missing or weak security headers
  2. Insecure session cookies
  3. Hardcoded secrets & API tokens in bodies
  4. Improper error handling (stack traces, DB errors, debug pages)
  5. Outdated / dev-mode technology fingerprints
  6. RCE-prone parameters in captured requests
- Active probing of the live target:
  7. Disclosure-path brute (.git/config/logs, CVE paths)
  8. Outdated / vulnerable software port probe
  9. Wayback forgotten-endpoint mining + re-probe
  10. Dependency confusion (sourcemaps -> npm)
- Runnable snippets
- Output
- Reference files: `references/secret-regexes.md`, `references/error-signatures.md`, `references/directory-signatures.md`

## Scope & authorization

Only run against a web application the user owns or is contractually engaged to test. This skill has two halves, each labelled by blast radius:

- **Passive (classes 1-6)** - inspects responses and requests you already captured (proxy history, an authenticated crawl, a saved sitemap, files on disk). Sends **no new HTTP requests**, mutates nothing, and is safe against a frozen capture.
- **Active (checks 7-10)** - sends live requests to the target and, for Wayback (9) and dependency confirmation (10), to third-party services (the Internet Archive, the npm registry). Check 8 is **aggressive** (multi-port sweep). Run these only in scope, and prefer a low-traffic window.

Take inputs as whatever you already hold: captured responses/requests for the passive half, and the target's domains / `domain:port` pairs for the active half. Report a finding exactly where each section says "Report a finding when...".

## Passive analysis of captured traffic

Each class is one pass over the capture; each yields per-target findings.

### 0. Extensive attack surface flushout and coverage

Work through the given scope of the target, and work to maximize all the attack surface expansion via all techniques that comes to mind.

Take a webapp for example - seeing a 404 is one thing, but subdomain enumeration expanding the targets, on each running available browser tools to get extended sitemap, available source code, backup files, flush errors and stack traces with unexpected inputs and more.

Coverage is most important as you have to invent and act on lots of variations per the target type tailored to any customization opportunity you may discover that can further the search (e.g. if its next.js version xyz do we know how to trigger errors on there).

### 1. Missing or weak security headers

For every captured response, case-insensitively collect the response header names and report each of the ten headers below that is absent. Report a finding when: a response is missing any of these. Remediation is "add the `<Header>` in responses returning from the server".

| Header | Why it matters |
|---|---|
| X-Frame-Options | Blocks embedding the site in iframes (clickjacking). |
| Content-Security-Policy | Controls which resources (e.g. JS) the page may load. |
| Strict-Transport-Security | Forces HTTPS, including on the initial HTTP connection. |
| Permissions-Policy | Disables specific browser APIs; propagates to iframes. |
| X-Content-Type-Options | Stops MIME sniffing; browser trusts only the declared Content-Type. |
| Referrer-Policy | Prevents leaking sensitive query params via the Referer to third parties. |
| Cross-Origin-Resource-Policy | (xs-leaks) Restricts cross-origin access to the document's resources. |
| Cross-Origin-Embedder-Policy | (xs-leaks) Blocks loading cross-origin resources that don't grant permission via CORP/CORS. |
| Cross-Origin-Opener-Policy | (xs-leaks) Stops popup/opener documents from reaching the page's global object. |
| Cache-Control | Prevents responses being cached in intermediary proxies and leaked to other users. |

### 2. Insecure session cookies

Look only at cookies that look session-bearing - the cookie **name** (case-insensitive) contains `sess` or `token`. For each such `Set-Cookie` with a non-empty value, report a finding when any of:

- **No attributes at all** - the cookie string has no `;` or no `=`. The whole cookie is insecure.
- **Missing `HttpOnly`** - `Cookie '<name>' doesn't have the HTTPOnly attribute set`. Fix: set HttpOnly on important cookies.
- **Missing `Secure`** - `Cookie '<name>' doesn't have the Secure attribute set`. Fix: set Secure.
- **`SameSite=None`** - `Cookie '<name>' has the SameSite attribute set to 'none'`. Fix: set SameSite=Strict, or at least Lax.
- **Long lifetime** - an `Expires` (parsed as a unix timestamp here) more than **14 days** out. `Cookie '<name>' has a long expiration date set of more than N days`. Fix: give important cookies a short-lived expiration.

Non-session cookies (no `sess`/`token` in the name) and empty-valued cookies are skipped to keep the signal high.

### 3. Hardcoded secrets & API tokens in bodies

Run the secret-regex set in `references/secret-regexes.md` over every captured response body. Report an INSIGHT (not a hard finding - these are high-value leads that need confirmation) when: any pattern matches. Record `<secret name> ( <matched value(s)> ) while requesting <paths>`; de-duplicate matches per target and truncate the joined match string to 400 chars.

The set (~40 patterns) covers provider credentials and identifiers: OpenAI/`sk-`, Slack tokens/webhooks, GitHub classic/fine-grained PATs and `git+https` URIs, AWS access-key IDs / MWS tokens / S3 bucket URLs, Google API keys / OAuth / GCP service-account JSON / `ya29.` access tokens, Stripe/Square/PayPal Braintree/Picatic/Mailgun/MailChimp payment & mail keys, DigitalOcean, HuggingFace, Grafana Cloud, Segment, Okta, Fullstory, Adobe, Heroku, Telegram, Firebase URLs, and a generic `"Password": "..."` JSON pattern. Full patterns in the reference file.

Secrets in third-party JS may be intentional publics (e.g. a Google Maps browser key) - verify before reporting as a leak; treat the finding as a lead.

### 4. Improper error handling - stack traces, DB errors, debug pages

Run the error-signature set in `references/error-signatures.md` over every captured response body. **First skip responses that are JavaScript bundles** (they produce false positives): skip the body if it matches `sourceMappingURL|window\.addEventListener|window\.document|webpackJsonp|jQuery`. Report a finding when: any signature matches; record `<signature name> ( <match> ) while requesting <paths>`.

The set (~130 signatures) spans: SQL/DB errors (MySQL, MSSQL OLE DB, PostgreSQL, Oracle `ORA-`, DB2, SQLite, MongoDB `E11000`, Elasticsearch `mapper_parsing_exception`); language runtime exceptions (Java `java.lang.*`/`java.io.*`/`java.sql.*`, Python `Traceback`/`ValueError`/`KeyError`/etc., Ruby, C++ `std::*`, .NET `System.*`); framework error pages (Django, Flask/Werkzeug, Laravel/Symfony, Spring/Spring Boot/Hibernate, Rails `ActionView::Template::Error`, Express.js, ASP.NET "Server Error in '/' Application"); **insecure-deserialization** gadget-chain traces (Java `ObjectInputStream.readObject`, `InvokerTransformer`/`LazyMap`/`ChainedTransformer`, `readObject`/`writeObject`, Python `pickle`/`yaml.load`, Ruby `Marshal.load`/`YAML.load`, PHP `unserialize(`, C# `BinaryFormatter`, Go `encoding/gob`, Jackson); config/secret disclosure (`<compilation debug="true"`, `DATABASES = {...}`, `secret_key_base`, Web.config, Node config errors); and CSRF/JWT errors. Full name+pattern table in the reference file.

### 5. Outdated / dev-mode technology fingerprints

Because this is a passive body-string search (not endpoint probing), the patterns are deliberately precise. Run over every captured response body; report an INSIGHT when any matches (`<name> ( <match> ) while requesting <paths>`). This is the passive counterpart to the active port probe in check 8 - a passive hit tells you where to point check 8.

| Fingerprint | Body regex | Meaning |
|---|---|---|
| Tomcat Dev Interface | `tomcat\.gif` | Default Tomcat page/manager exposed. |
| Jenkins Dev Interface | `Welcome to Jenkins!` | Jenkins UI reachable. |
| Werkzeug Dev Interface | `Werkzeug powered traceback interpreter` | Flask/Werkzeug interactive debugger (RCE-prone). |
| AD Self Service Management Server | `adscsrf` | ADSelfService Plus portal. |
| Frontend Code Editor | `ace_editor|codemirror|monaco-editor` | In-page code editor surface. |
| Pulse Secure File Read | `dana-na\/css` | Pulse Secure (CVE-2019-11510 arbitrary file read). |

Reference: nuclei-templates `http/cves/2019/CVE-2019-11510.yaml`.

### 6. RCE-prone parameters in captured requests

Over captured **requests**, inspect query-string params and body params (parsed by Content-Type: XML tags, JSON keys, or `x-www-form-urlencoded` keys). Report an INSIGHT when: a parameter **key** matches the RCE-suggestive wordlist below, or a parameter **value** equals `##class` (a common template/gadget marker). Record which location matched, e.g. `Query parameter key 'cmd' was found at path '<path>'` or `JSON body parameter key 'exec' was found at path '<path>'`. These names don't prove a vuln - they flag parameters worth targeted, authorized command-injection testing.

Parameter keys (case-sensitive match, as captured):
```
daemon, execute, cmd, cli, ip, xp_cmdshell, CSPCHD, exec, func, function,
command, eval, shell, shell_exec, popen, proc_open, bash, python, system,
payload, cmdline, exe, execcommand, exec_code, exec_cmd, executeshell,
cmd_exec, cmd_inject, cmd_shell, cmd_script, run, runcmd, runcommand,
shellcode, shellexec, shellcmd, command_prompt, process, terminal,
execute_command, exec_file, load_module, load_script, proc_cmdline,
runtime_exec, shell_execute, system_command, sys_command
```
Parameter values: `##class`.

Requests with a body but no Content-Type, or an unrecognized Content-Type, are skipped.

## Active probing of the live target

These send live requests. Confirm scope first; they leave logs on the target (and, for 9-10, on third parties).

### 7. Disclosure-path brute - *active*

For each domain, GET a curated set of source/config/log/known-CVE paths (no redirects, TLS verification off, 10s). Each signature carries a path, an expected **status regex** and a **body regex**; a match must satisfy both. The full 26-signature table lives in `references/directory-signatures.md` (e.g. `/.git/config`, `/.git/config~`, `/.git`, `/.svn`, `/package.json`, `/jsconfig.json`, `/config.json`, `/info.php`, `/phpinfo.php`, `/web.config`, `/global.asa`, `/storage/logs/laravel.log`, `/wp-content/debug.log`, Eclipse Jetty `/WEB-INF/web.xml` and its `/%2e/`-prefixed traversal variant, Telerik `/Telerik.Web.UI.WebResource.axd?type=rau`, Pulse `/dana-na/`, `/adminer.php`, `/phinx.yml`, and Django-debug `/djgo` expecting a **4xx** page containing `DEBUG =`).

**False-positive controls (essential - catch-all responders are common):**
- Drop any response whose body contains `404 Not Found` or `This page can't be displayed`.
- Hash (MD5) each matching response body; collapse duplicates so one generic page counted many times becomes one hit.
- If **every** signature matches (hit count == number of signatures), discard the whole domain - the server answers 200 to everything and none of it is real.

**Report a finding when**: at least one - but not all - signatures match after dedup. Severity: Information Disclosure (rate individually; exposed `.git`/`.svn` or a live source/config/log file is often high impact).

### 8. Outdated / vulnerable software port probe - *aggressive*

For each `domain`, probe management interfaces of RCE-prone stacks across their typical ports, matching a substring in the response body. Cache each port's response and reuse it so overlapping port lists don't re-request. On 80/443 use plain `http://host` / `https://host`; on any other port try **both** http and https (label the hit `port (HTTP)` / `port (HTTPS)`).

| Service (RCE surface) | Path | Body substring | Ports |
|---|---|---|---|
| Apache Tomcat | `/` | `Tomcat` | 80, 443, 8080, 8081, 8089, 8090, 9090, 9091 |
| Jenkins | `/login` | `Jenkins` | 80, 443, 8080, 8081, 8089, 8090, 9090, 9091, 7443, 8443, 9443 |
| Werkzeug debugger | `/console` | `Werkzeug powered traceback interpreter` | 80, 443, 8080, 4000 |
| ManageEngine ADSelfService | `/showLogin.cc` | `adscsrf` | 8080, 8889 |
| InterSystems Caché | `/` | `CSPCHD` | 443 |
| ASP.NET ViewState | `/` | `__VIEWSTATE` | 443 |
| Jolokia / Java RMI | `/jolokia` | `jolokia` | 443 |
| Plone CMS | `/` | `Plone` | 443 |

**Report a finding when**: any service substring appears. Severity: potential RCE insight - report `"<service> found on port(s) <list>"`. The Werkzeug `/console` interactive debugger and an exposed Jolokia agent are directly exploitable; the rest are version-review leads.

### 9. Wayback forgotten-endpoint mining + re-probe - *active-3rdparty (harvest), then active (re-probe)*

**Harvest (active-3rdparty).** Query the Internet Archive CDX index per domain:
```
https://web.archive.org/cdx/search/cdx?url={domain}*&collapse=urlkey&limit=40000&fl=urlkey,timestamp,original,mimetype,statuscode,length&output=json
```
The archive rate-limits ~15 req/min and imposes a ~5-minute block; use a retry/backoff of 5m -> 10m -> 20m on HTTP 429. Drop static/noise by mimetype (`application/javascript`, `text/javascript`, `application/x-javascript`, `text/css`, `image/png`, `image/gif`, `font/woff`) and by extension in the URL (`.png .js .css .jpg .JPG .jpeg .gif .ttf .tif .eot .woff .woff2 .pdf .otf .svg .ico .html .swf .styl`).

**Filter to the interesting URLs** (this de-noising is the point):
- Skip URLs longer than 1000 chars.
- Skip "cache-buster / hash-looking" URLs: those matching `^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?!.*\?)(?!.*\=)(?!.*\&)(?!.*\_).*$` (mixed-case+digits, no query, no underscore).
- Skip boring path substrings: `how-it-works, promotions, images, google-tag-manager-cart, terms-of, -products, frequently-asked-questions, wewaw, news, wp-json, wp-admin, diy`.
- Require a query string (`?...`); keep only **distinct** `(path, frozenset(query-param-names))` tuples.
- Compute the 20 most common path segments / query-param names across the survivors; keep an element only if it recurs **>5** times and its length is **>3 and <11**; then **drop** any URL that contains one of those common elements - i.e. keep the *unusual* endpoints, discard the site-wide boilerplate.

Report the surviving URL set as an Information Disclosure insight (archived, possibly-still-live parameterized endpoints worth manual testing).

**Targeted re-probe (active).** For any surviving URL whose path matches a fingerprint below, send a live GET (5s) to the target and match status + body:

| Fingerprint | Path regex | Status | Body regex | Meaning |
|---|---|---|---|---|
| Telerik Web UI | `Telerik` | `2.*` | `.*` | Telerik UI present (CVE-2019-18935 lineage) |
| Java Server Pages / JSF | `javax\.faces\.resource\|javax\.faces\.ViewState` | `2.*` | `.*` | JSF ViewState deserialization surface |
| Pulse Secure File Read | `dana-na` | `2.*` | `dana-na\/css` | Pulse Connect Secure (CVE-2019-11510 arbitrary file read) |

**Report a finding when**: a re-probed URL still returns 2xx and matches its body regex. Severity: potential RCE (HIGH) - flag `URL (fingerprint description)` for manual confirmation.

### 10. Dependency confusion: sourcemap -> company package -> npm - *active (harvest), active-3rdparty (registry check)*

**Harvest private package names (active).** For each `domain:port` on 443, GET the homepage, parse every `<script src="...js">`, and request the co-located sourcemap `src + ".map"` (resolved absolute). Only fetch maps whose URL is same-origin (`domain in map_url`). From each map's `sources[]`, keep entries containing `../node_modules`; take the path after `../node_modules/`, split on `/`, and:
- skip if the first segment is `src`;
- drop any `.js` segment;
- if two-or-more segments remain: when the second segment is one of `src, es, esm, lib, dist, node_modules`, the package is just the **first** segment; otherwise treat it as a **scoped** package `@scope/name` -> `{seg0}/{seg1}`.

Keep a derived package name as a **company-owned candidate** when `fuzz.ratio(package_name, company_root_label) > 50` (thefuzz / Levenshtein-style ratio) - i.e. the dependency's name resembles the company's own name, the tell-tale of an internal package leaked into a public bundle.

**Confirm dependency confusion (active-3rdparty).** For each candidate, query the public npm registry (`https://registry.npmjs.org/<name>`, URL-encode a `@scope/name`). If it returns **404 / not found**, the private name is unclaimed on the public registry and is **dependency-confusion-claimable**; if it resolves to a package not owned by the target org, it may already be squatted.

**Report a finding when**: a company-resembling `node_modules` package name derived from a target's own bundle is **unregistered** (or third-party-owned) on the public registry. Severity: dependency confusion (a malicious publish of that name can execute in the target's build/runtime). Also flag the exposed sourcemaps themselves as an information-disclosure insight.

## Runnable snippets

### Passive sweep (no requests sent)

These operate on a directory of already-captured responses/requests. Assume `CAPDIR` holds saved response bodies (e.g. `*.body`) and sibling `*.headers` files. Adapt the loader to your proxy's export format.

```bash
# 1 missing security headers - scan saved *.headers files (name: value per line)
python3 - "$CAPDIR" <<'PY'
import sys,glob,os
req=["x-frame-options","content-security-policy","strict-transport-security","permissions-policy","x-content-type-options","referrer-policy","cross-origin-resource-policy","cross-origin-embedder-policy","cross-origin-opener-policy","cache-control"]
for f in glob.glob(os.path.join(sys.argv[1],"*.headers")):
    have={l.split(":",1)[0].strip().casefold() for l in open(f,errors="ignore") if ":" in l}
    miss=[h for h in req if h not in have]
    if miss: print(os.path.basename(f),"MISSING:",", ".join(miss))
PY
```
```bash
# 2 insecure session cookies - parse Set-Cookie lines from saved *.headers
python3 - "$CAPDIR" <<'PY'
import sys,glob,os,time
for f in glob.glob(os.path.join(sys.argv[1],"*.headers")):
    for line in open(f,errors="ignore"):
        if not line.lower().startswith("set-cookie:"): continue
        c=line.split(":",1)[1].strip()
        if ";" not in c or "=" not in c: print(os.path.basename(f),"no-attributes:",c); continue
        parts=c.split(";"); nv=parts[0]; name=nv.split("=")[0]
        if not nv.split("=",1)[1]: continue
        if not any(p in name.casefold() for p in ("sess","token")): continue
        attrs=[a.strip().casefold() for a in parts[1:] if a.strip()]
        if "httponly" not in attrs: print(os.path.basename(f),name,"missing HttpOnly")
        if "secure" not in attrs:   print(os.path.basename(f),name,"missing Secure")
        if "samesite=none" in attrs:print(os.path.basename(f),name,"SameSite=None")
        for a in attrs:
            if a.startswith("expires="):
                try:
                    d=(int(a.split("=",1)[1])-time.time())/86400
                    if d>14: print(os.path.basename(f),name,"long expiry %d days"%d)
                except ValueError: pass
PY
```
```bash
# 3 secrets  &  4 error signatures  &  5 tech fingerprints - one regex sweep over bodies
# put the patterns from references/secret-regexes.md + references/error-signatures.md into rules.txt
# as "name<TAB>pattern" lines (prefix error-signature names with ERR:), then:
python3 - "$CAPDIR" rules.txt <<'PY'
import sys,glob,os,re
rules=[l.rstrip("\n").split("\t",1) for l in open(sys.argv[2]) if "\t" in l]
rules=[(n,re.compile(p)) for n,p in rules]
jsskip=re.compile(r"sourceMappingURL|window\.addEventListener|window\.document|webpackJsonp|jQuery")
for f in glob.glob(os.path.join(sys.argv[1],"*.body")):
    body=open(f,errors="ignore").read()
    isjs=bool(jsskip.search(body))
    for n,rx in rules:
        if isjs and n.startswith("ERR:"): continue
        m=rx.findall(body)
        if m:
            j=", ".join(sorted({x if isinstance(x,str) else "".join(x) for x in m}))
            print(os.path.basename(f),n,"(",(j[:400]+"..") if len(j)>400 else j,")")
PY
```
```bash
# 6 RCE-prone parameters - scan captured requests (one URL per line in reqs.txt)
python3 - reqs.txt <<'PY'
import sys,urllib.parse as u
keys={"daemon","execute","cmd","cli","ip","xp_cmdshell","CSPCHD","exec","func","function","command","eval","shell","shell_exec","popen","proc_open","bash","python","system","payload","cmdline","exe","execcommand","exec_code","exec_cmd","executeshell","cmd_exec","cmd_inject","cmd_shell","cmd_script","run","runcmd","runcommand","shellcode","shellexec","shellcmd","command_prompt","process","terminal","execute_command","exec_file","load_module","load_script","proc_cmdline","runtime_exec","shell_execute","system_command","sys_command"}
for line in open(sys.argv[1]):
    q=u.urlsplit(line.strip()).query
    for k,vs in u.parse_qs(q).items():
        if k in keys: print("param key",k,"in",line.strip())
        for v in vs:
            if v=="##class": print("param value ##class in",line.strip())
PY
```

### Active probes (live requests to the target)

```bash
# 7 disclosure-path brute (no redirects; flag 2xx that isn't a soft-404)
for p in /.git/config /.git/config~ /.svn /package.json /jsconfig.json /config.json /info.php /phpinfo.php /web.config /global.asa /storage/logs/laravel.log /wp-content/debug.log /adminer.php /phinx.yml; do
  code=$(curl -sk -o /tmp/b -w '%{http_code}' --max-time 10 "https://TARGET$p")
  grep -qE '404 Not Found|This page can.t be displayed' /tmp/b || { [ "${code:0:1}" = 2 ] && echo "$code  $p  md5=$(md5 -q /tmp/b 2>/dev/null || md5sum /tmp/b|cut -d' ' -f1)"; }
done   # if EVERY path returns 2xx, the host is a catch-all -> discard all

# 8 outdated / vulnerable software port probe (body-substring per service)
for pp in "80 / Tomcat" "8080 /login Jenkins" "8080 /console Werkzeug" "443 /jolokia jolokia" "443 / __VIEWSTATE" "443 / Plone"; do
  set -- $pp; port=$1; path=$2; sig=$3
  for sch in http https; do curl -sk --max-time 5 "$sch://TARGET:$port$path" | grep -q "$sig" && echo "$sig on $port ($sch)"; done
done

# 9 Wayback harvest (respect 15/min; back off on 429)
curl -s "https://web.archive.org/cdx/search/cdx?url=example.com*&collapse=urlkey&limit=40000&fl=urlkey,timestamp,original,mimetype,statuscode,length&output=json" \
 | python3 -c 'import sys,json;[print(r[2]) for r in json.load(sys.stdin)[1:] if r[3] not in ("application/javascript","text/javascript","application/x-javascript","text/css","image/png","image/gif","font/woff") and "?" in r[2]]'

# 10 sourcemap -> node_modules -> npm ownership
curl -s https://TARGET/ | grep -oE 'src="[^"]+\.js"' | sed 's/src="//;s/"//' | while read s; do
  curl -s "https://TARGET/${s}.map" | python3 -c 'import sys,json;
d=json.load(sys.stdin);
[print(x.split("../node_modules/")[1]) for x in d.get("sources",[]) if "../node_modules" in x]' 2>/dev/null; done | sort -u
curl -s -o /dev/null -w '%{http_code}\n' "https://registry.npmjs.org/CANDIDATE_PACKAGE"   # 404 => dependency-confusion-claimable
```

## Output

Finish with a two-part ledger, then a verdict:
- **Passive** - per class (1-6): targets scanned and findings, distinguishing confirmed **findings** (classes 1, 2, 4) from **insights/leads** (classes 3, 5, 6) that need authorized follow-up.
- **Active** - per check (7-10): check / blast-radius / done? / result, with the confirming request for each hit (exposed path after dedup/catch-all filtering, fingerprinted software+port, still-live archived endpoint, claimable private package).

Report each class/check's true status - scanned, run, not-run, or rate-limited - so coverage is honest. A clean passive result only means the leak wasn't present *in the captured traffic*; never present an un-run or rate-limited active check as clean.

## references

```

```

## references/directory-signatures.md

# Disclosure-path signatures (Step 3)

Full signature table for the disclosure-path brute. Request each with GET (unless noted),
allow_redirects=False, verify=False, 10s timeout. A hit requires BOTH the status regex and the
body regex to match. Then: drop bodies containing `404 Not Found` or `This page can't be
displayed`; MD5-dedup bodies; and if EVERY signature matches, discard the whole host as a
catch-all responder.

| Occurrence name | Method | Path | Status | Body regex |
|---|---|---|---|---|
| package.json Disclosure | GET | `/package.json` | `2.*` | `.*` |
| jsconfig.json Disclosure | GET | `/jsconfig.json` | `2.*` | `.*` |
| Git Config 1 | GET | `/.git/config` | `2.*` | `.*` |
| Git Config 2 | GET | `/.git/config~` | `2.*` | `.*` |
| .git Disclosure | GET | `/.git` | `2.*` | `.*` |
| .svn Disclosure | GET | `/.svn` | `2.*` | `.*` |
| config.json Disclosure | GET | `/config.json` | `2.*` | `.*` |
| idx_config Directory Listing | GET | `/idx_config/` | `2.*` | `.*` |
| info.php Disclosure | GET | `/info.php` | `2.*` | `.*` |
| phpinfo Disclosure | GET | `/phpinfo.php` | `2.*` | `.*` |
| Jira File Read | GET | `/s/lkx/_/;/META-INF/maven/com.atlassian.jira/jira-webapp-dist/pom.properties` | `2.*` | `.*` |
| Eclipse Jetty Info Disclosure 1 (CVE-2021-28164) | GET | `/WEB-INF/web.xml` | `2.*` | `(?=.*<\/web-app>)(?=.*java.sun.com)` |
| Eclipse Jetty Info Disclosure 2 (CVE-2021-28164) | GET | `/%2e/WEB-INF/web.xml` | `2.*` | `(?=.*<\/web-app>)(?=.*java.sun.com)` |
| IIS Web Config | GET | `/web.config` | `2.*` | `.*` |
| Laravel App Log | GET | `/storage/logs/laravel.log` | `2.*` | `.*` |
| Zend App Init | GET | `/application/configs/application.ini` | `2.*` | `.*` |
| Wordpress debug.log | GET | `/wp-content/debug.log` | `2.*` | `.*` |
| Wordpress File Upload Plugin Logs | GET | `/wp-content/uploads/file-manager/log.txt` | `2.*` | `.*` |
| log.txt Disclosure | GET | `/log.txt` | `2.*` | `.*` |
| global.asa Disclosure | GET | `/global.asa` | `2.*` | `.*` |
| Pulse Secure | GET | `/dana-na/` | `2.*` | `by Pulse Secure, LLC` |
| Telerik UI (CVE-2019-18935 lineage) | GET | `/Telerik.Web.UI.WebResource.axd?type=rau` | `2.*` | `RadAsyncUpload handler is registered successfully` |
| Phinx Disclosure | GET | `/phinx.yml` | `2.*` | `.*` |
| Adminer SSRF | GET | `/adminer.php` | `2.*` | `.*` |
| Django in Debug Mode | GET | `/djgo` | `4.*` | `DEBUG =` |

Note the deliberately fingerprinted-by-body entries (Jetty web.xml, Telerik axd, Pulse, Django)
narrow to a real match; the `.*`-body entries fire on any 2xx, so the soft-404 filter, MD5 dedup,
and all-match catch-all discard are what keep them honest.

## references/error-signatures.md

# Error-page / stack-trace / debug-disclosure signatures (class 4)

Run over each captured response body **after** skipping JS bundles (body matches `sourceMappingURL|window\.addEventListener|window\.document|webpackJsonp|jQuery`). Any match is an "Improper Error Handling" finding: verbose errors leak stack frames, file paths, framework/DB versions, and gadget-chain internals. When loading into the `rules.txt` sweep, prefix each name with `ERR:` so the snippet skips them on JS bundles.

## Contents
- Database / SQL errors
- Language runtime exceptions
- Framework error pages
- Insecure-deserialization / gadget-chain traces
- Config / secret disclosure & debug mode
- CSRF / JWT / auth errors

## Database / SQL errors
| Name | Regex |
|---|---|
| Generic SQL error | `Database error: SQLSTATE` |
| Microsoft SQL Server error | `Microsoft OLE DB Provider for SQL Server` |
| MySQL syntax error | `You have an error in your SQL syntax` |
| SQL Injection Error | `SQL syntax error` |
| PostgreSQL error | `ERROR:  syntax error at or near` |
| Oracle SQL error | `ORA-\d+:` |
| DB2 SQL error | `DB2 SQL Error: SQLCODE=` |
| SQLite error | `SQLiteException` |
| MongoDB error | `E11000 duplicate key error collection` |
| Elasticsearch error | `type=\\\"mapper_parsing_exception\\\"` |

## Language runtime exceptions
| Name | Regex |
|---|---|
| Java exception | `java\.lang\.\w+Exception` |
| Java RuntimeException | `java\.lang\.RuntimeException` |
| NullPointerException | `java\.lang\.NullPointerException` |
| ArrayIndexOutOfBoundsException | `java\.lang\.ArrayIndexOutOfBoundsException` |
| ClassCastException | `java\.lang\.ClassCastException` |
| Java NumberFormatException | `java\.lang\.NumberFormatException` |
| Java StringIndexOutOfBoundsException | `java\.lang\.StringIndexOutOfBoundsException` |
| Java IllegalStateException | `java\.lang\.IllegalStateException` |
| Java ClassNotFoundException | `java\.lang\.ClassNotFoundException` |
| Java IOException | `java\.io\.IOException` |
| Java EOFException | `java\.io\.EOFException` |
| Java SQLException | `java\.sql\.SQLException` |
| PHP Parse error | `PHP Parse error` |
| PHP Warning | `PHP Warning` |
| PHP Notice | `PHP Notice` |
| Generic fatal error | `Fatal error` |
| Python Traceback error | `Traceback \(most recent call last\)` |
| Python ValueError | `ValueError:` |
| Python TypeError | `TypeError:` |
| Python ImportError | `ImportError:` |
| Python AttributeError | `AttributeError:` |
| Python KeyError | `KeyError:` |
| Python IndexError | `IndexError:` |
| Python MemoryError | `MemoryError` |
| Python RecursionError | `RecursionError` |
| Python ModuleNotFoundError | `ModuleNotFoundError` |
| Python OSError | `OSError` |
| Python OverflowError | `OverflowError` |
| Python ZeroDivisionError | `ZeroDivisionError` |
| Ruby error | `/[^\s]+.rb:\d+:` |
| Ruby ArgumentError | `ArgumentError` |
| Ruby LoadError | `cannot load such file --` |
| Ruby NameError | `: undefined local variable or method` |
| C++ std::exception | `std::exception` |
| C++ std::length_error | `std::length_error` |
| C++ std::bad_alloc | `std::bad_alloc` |
| .NET InvalidOperationException | `System\.InvalidOperationException` |
| NullReferenceException | `System\.NullReferenceException` |
| IndexOutOfRangeException | `System\.IndexOutOfRangeException` |

## Framework error pages
| Name | Regex |
|---|---|
| Ruby on Rails error | `ActionView::Template::Error` |
| Ruby on Rails Stack Trace | `^.+Error: .+\n(\s+at .+\:\d+(:\d+)?\n)+` |
| Ruby on Rails Database Error | `ActiveRecord::(.+?)Error` |
| Ruby on Rails Strong Parameters Error | `ActionController::ParameterMissing: param is missing or the value is empty` |
| Django ImportError | `ImportError: No module named` |
| Django OperationalError | `django.db.utils.OperationalError` |
| Django InterfaceError | `django.db.utils.InterfaceError` |
| Django FieldError | `django.core.exceptions.FieldError` |
| Django ValidationError | `value has an invalid format. It must be` |
| Django ObjectDoesNotExist | `django.core.exceptions.ObjectDoesNotExist` |
| Python Django Query Error | `FieldError: Cannot resolve keyword '.+' into field` |
| Python Django Technical Error | `Traceback \(most recent call last\):\n(\s+File \".+\", line \d+, in .+\n)+` |
| Flask KeyError | `KeyError: 'foo'` |
| Flask RuntimeError | `RuntimeError: Working outside of application context` |
| Flask Werkzeug Error | `werkzeug.exceptions.HTTPException` |
| Flask SQLAlchemy Error | `sqlalchemy.exc.InvalidRequestError` |
| Flask JWTExtendedException | `flask_jwt_extended.exceptions.JWTExtendedException` |
| Laravel ErrorException | `ErrorException in.*\.php` |
| Laravel QueryException | `Illuminate\\Database\\QueryException` |
| PHP Laravel Detailed Error | `#(\d+ )?\/path\/to\/file\.php\((\d+)\): .+\nStack trace:\n.+\n` |
| PHP Laravel Database Error | `PDOException: SQLSTATE\[\w+\]: .+` |
| Symfony FatalErrorException | `Symfony\\Component\\Debug\\Exception\\FatalErrorException` |
| Symfony NotFoundHttpException | `Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException` |
| PHP Laravel Middleware Error | `Symfony\\Component\\HttpKernel\\Exception\\HttpException` |
| Spring NoSuchBeanDefinitionException | `org\.springframework\.beans\.factory\.NoSuchBeanDefinitionException` |
| Spring BeanCreationException | `org\.springframework\.beans\.factory\.BeanCreationException` |
| Spring DataIntegrityViolationException | `org.springframework.dao.DataIntegrityViolationException` |
| Spring HibernateJdbcException | `org.hibernate.exception.GenericJDBCException` |
| Java Spring Data Source Error | `org\.springframework\.jdbc\.CannotGetJdbcConnectionException` |
| Java Spring Data Access Error | `DataAccessException: .+` |
| Java Spring Bean Configuration | `Error creating bean with name '.+' defined in .+: .+` |
| Java Spring Boot Error | `org\.springframework\.boot\|Caused by:` |
| ASP.NET HttpException | `System.Web.HttpException` |
| ASP.NET HttpParseException | `System.Web.HttpParseException` |
| ASP.NET SqlException | `System.Data.SqlClient.SqlException` |
| ASP.NET FileNotFoundException | `System.IO.FileNotFoundException` |
| ASP.NET Detailed Error Page | `Server Error in '/.*' Application` |
| Express.js CastError | `CastError: Cast to ObjectId failed` |
| Express.js Detailed Error | `at .* \(.+:\d+:\d+\)` |
| Express.js Middleware Error | `Error: (Failed to find|Cannot find) module .+` |
| Express.js MongoError | `MongooseServerSelectionError` |
| Node.js MongoDB Error | `MongoError: .+` |
| Custom Class Method Invocation | `\w+\.\w+\.main.*.java\:` |

## Insecure-deserialization / gadget-chain traces
| Name | Regex |
|---|---|
| Go Deserialization Error (gob) | `gob: type not registered for interface` |
| Golang encoding/gob Decoding Error | `encoding/gob` |
| Java Hashtable.readObject Error | `java\.util\.Hashtable\.readObject` |
| Java Hashtable.reconstitutionPut Error | `java\.util\.Hashtable\.reconstitutionPut` |
| Apache Commons AbstractMapDecorator.equals Error | `org\.apache\.commons\.collections\.map\.AbstractMapDecorator\.equals` |
| Java AbstractMap.equals Error | `java\.util\.AbstractMap\.equals` |
| Apache Commons LazyMap.get Error | `org\.apache\.commons\.collections\.map\.LazyMap\.get` |
| Apache Commons ChainedTransformer.transform Error | `org\.apache\.commons\.collections\.functors\.ChainedTransformer\.transform` |
| Apache Commons InvokerTransformer.transform Error | `org\.apache\.commons\.collections\.functors\.InvokerTransformer\.transform` |
| Java Reflection Method.invoke Error | `java\.lang\.reflect\.Method\.invoke` |
| Java DelegatingMethodAccessorImpl.invoke Error (sun) | `sun\.reflect\.DelegatingMethodAccessorImpl\.invoke` |
| Java NativeMethodAccessorImpl.invoke Error (sun) | `sun\.reflect\.NativeMethodAccessorImpl\.invoke` |
| Java NativeMethodAccessorImpl.invoke0 Error (sun) | `sun\.reflect\.NativeMethodAccessorImpl\.invoke0` |
| Java NativeMethodAccessorImpl.invoke Error (jdk) | `jdk\.internal\.reflect\.NativeMethodAccessorImpl\.invoke` |
| Java DelegatingMethodAccessorImpl.invoke Error (jdk) | `jdk\.internal\.reflect\.DelegatingMethodAccessorImpl\.invoke` |
| Java Runtime.exec Error | `java\.lang\.Runtime\.exec` |
| Java WriteAbortedException Error | `java\.io\.WriteAbortedException: writing aborted; java\.io\.NotSerializableException` |
| Java NotSerializableException Error | `java\.io\.NotSerializableException: \w+\.\w+` |
| Java ObjectOutputStream.writeObject0 Error | `java\.io\.ObjectOutputStream\.writeObject0` |
| Java ObjectInputStream.readObject Error | `java\.io\.ObjectInputStream\.readObject` |
| Java ArrayList.writeObject Error | `java\.util\.ArrayList\.writeObject` |
| Java ArrayList.readObject Error | `java\.util\.ArrayList\.readObject` |
| Java ObjectStreamClass.invokeReadObject Error | `java\.io\.ObjectStreamClass\.invokeReadObject` |
| Java ObjectOutputStream.writeSerialData Error | `java\.io\.ObjectOutputStream\.writeSerialData` |
| Java ObjectInputStream.readOrdinaryObject Error | `java\.io\.ObjectInputStream\.readOrdinaryObject` |
| Java ObjectOutputStream.writeObject Error | `java\.io\.ObjectOutputStream\.writeObject` |
| Java ObjectStreamClass.invokeWriteObject Error | `java\.io\.ObjectStreamClass\.invokeWriteObject` |
| Java ObjectOutputStream.writeOrdinaryObject Error | `java\.io\.ObjectOutputStream\.writeOrdinaryObject` |
| Java Serialization Error | `java\.io\.Object(I\|O)Stream` |
| Python pickle Unpickling Error | `pickle\.UnpicklingError` |
| Python PyYAML Deserialization Error | `yaml\.load\(` |
| PHP unserialize Error | `unserialize\(` |
| Ruby YAML.load Error | `YAML\.load\(` |
| Ruby on Rails Marshal.load Error | `Marshal\.load` |
| C# BinaryFormatter Deserialization Error | `System\.Runtime\.Serialization\.Formatters\.Binary\.BinaryFormatter` |
| Perl Storable Error | `Storable::thaw` |
| Django PickleField Unpickling Error | `django\.db\.models\.fields\.PickleField` |
| Flask-Session Pickle Deserialization Error | `flask_session\.sessions\.TaggedJSONSerializer` |
| Spring Framework Java Deserialization Error | `org\.springframework\.core\.deserialize\.` |
| Node.js unserialize-javascript Error | `unserialize-javascript` |
| Java Jackson JSON Deserialization Error | `com\.fasterxml\.jackson\.databind` |
| Command Injection Error | `Command execution error` |

## Config / secret disclosure & debug mode
| Name | Regex |
|---|---|
| ASP.NET Web.config Error (debug) | `<compilation debug=\"true\"` |
| ASP.NET Web Config comment | `<!--\s*Web\.Config Configuration File\s*-->` |
| Django Debug = True enabled | `your Django settings file. Change that to` |
| Python Django Database Configuration | `DATABASES = \{.+}` |
| Ruby on Rails Secret Key Base | `config\.secrets\.secret_key_base` |
| Node.js Configuration Disclosure | `ConfigurationError: \|Failed to read config file: .+\.json` |
| Node.js Invalid Configuration | `Error: Invalid configuration: .+` |
| Java Spring Security Misconfiguration | `org\.springframework\.security` |

## CSRF / JWT / auth errors
| Name | Regex |
|---|---|
| Express.js CSRF Token Error | `TokenMismatchException: CSRF token mismatch` |
| Ruby on Rails CSRF Error | `ActionController::InvalidAuthenticityToken` |
| Python Django CSRF Error | `CSRF verification failed` |
| PHP Laravel CSRF Token Error | `TokenMismatchException in VerifyCsrfToken.php` |
| Java Spring Security CSRF Error | `Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'` |
| Node.js JWT Error | `JsonWebTokenError: .+` |
| Express.js JWT Error | `UnauthorizedError: .+` |

Note: the source list contained several exact duplicates (e.g. `java.io.IOException`, `Illuminate\\Database\\QueryException`, `ObjectOutputStream.writeOrdinaryObject`, `NativeMethodAccessorImpl.invoke` in both `sun.reflect` and `jdk.internal.reflect`) - collapsed to one row each here without losing any distinct pattern.

## references/secret-regexes.md

# Secret / API-token regexes (class 3)

Run every pattern over each captured response body. A match is a lead ("Hardcoded Secrets" insight), not a confirmed leak - verify the value is a real, private credential (some browser-side keys are intentionally public). De-duplicate matches per target; truncate the joined match string to 400 chars. (Illustrative example values were removed so the table ships no scannable tokens.)

| Name | Regex |
| --- | --- |
| Generic password | `\", ?\"Password\": ?\"[a-zA-Z].{4,32}?\"` |
| Fullstory API token | `na1\.by0x[a-zA-Z0-9]{84,100}\/[A-Z0-9]{7}` |
| Okta API token | `['\" ]00[a-zA-Z0-9\-\_]{40}['\" ]` |
| Segment API token | `sgp_[a-zA-Z0-9]{64}` |
| DigitalOcean user access token | `do(p\|o)_v1_[a-f0-9]{64}` |
| HuggingFace user access token | `hf_[a-zA-Z]{34}` |
| Grafana Cloud API token | `glc_ey([a-zA-Z0-9_-]{90})` |
| Adobe OAuth client secret | `(?i)\b(p8e-[a-z0-9-]{32})(?:[^a-z0-9-]\|$)` |
| ChatGPT / OpenAI API key | `sk-(?=[^\d]*\d)(?=[^\w]*[a-zA-Z])[\w\d]{48}` |
| Homebrew artifactory API token | `(?:\s\|=\|:\|"\|^)AKC[a-zA-Z0-9]{10,}` |
| Slack app token | `xox[baprso]-[0-9a-zA-Z]{10,48}(?:-[0-9a-zA-Z]{10,48})?` |
| GitHub access token (Classic) | `gh(o\|p\|u)_[a-zA-Z0-9]{36}` |
| GitHub access token (Fine-grained) | `github_pat_[0-9a-zA-Z_]{82}` |
| GitHub URI (creds in URL) | `git\+https:\/\/\w+:\w+@github\.com\/\w+\/\w+(-\w+)*\.git` |
| AWS S3 bucket | `(?:[A-Za-z0-9.-]{3,63}\.s3\.(?:amazonaws\.com\|customdomain\.com)\|s3(?:-global)?\.amazonaws\.com/[A-Za-z0-9.-]{3,63}\|[A-Za-z0-9.-]{3,63}\.s3\.[A-Za-z0-9-]+\.amazonaws\.com)(?:/.+?(?= \|\,\|\"\|\?))` |
| Firebase URL | `https?://(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]\|-){0,61}[0-9A-Za-z])\.firebaseio\.com` |
| Slack Token | `(xox[p\|b\|o\|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})` |
| Amazon AWS Access Key ID | `(?!.*EXAMPLE)AKIA[0-9A-Z]{16}` |
| Amazon MWS Auth Token | `amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}` |
| Facebook Access Token | `EAACEdEose0cBA[0-9A-Za-z]+` |
| Google API Key | `AIza[0-9A-Za-z\\-_]{35}` |
| Google Cloud Platform OAuth | `[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com` |
| Google (GCP) service-account | `\"type\": \"service_account\"` |
| Google OAuth Access Token | `ya29\\.[0-9A-Za-z\\-_]+` |
| Heroku API Key | `(?i)heroku.*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}` |
| MailChimp API Key | `[0-9a-f]{32}-us[0-9]{1,2}` |
| Mailgun API Key | `key-[0-9a-zA-Z]{32}` |
| PayPal Braintree Access Token | `access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}` |
| Picatic API Key | `sk_live_[0-9a-z]{32}` |
| Slack Webhook | `https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}` |
| Stripe API Key | `(s\|r)k_(live\|test)_[0-9a-zA-Z]{24,255}` |
| Square Access Token | `sq0atp-[0-9A-Za-z\\-_]{22}` |
| Square OAuth Secret | `sq0csp-[0-9A-Za-z\\-_]{43}` |
| Telegram Bot Token | `bot\d{6}:[A-Za-z0-9_-]{34}` |

Notes: the original set also duplicated the `AIza...` Google-key pattern under several product labels (GCP API Key, Drive, Gmail) and the `...apps.googleusercontent.com` OAuth pattern under Drive/Gmail - they are the same two regexes, listed once here. When loading into `rules.txt` for the sweep snippet, prefix these names with a non-`ERR:` tag so they are never skipped on JS bundles.

