Files
edgeguard-owasp-feed/CVE-CHANNEL.md
T
AndreandClaude Opus 5 d8426a1d6c Document importing from a Nuclei template, and what the importer refuses
A CVE record describes a vulnerability, not a request, which is why this
channel is written by hand. A Nuclei template contains the request — path,
query, headers, body — because that is what it sends, so for thousands of CVEs
the shape a virtual patch needs is already written down.

What cannot come across is the template's matchers: they read the RESPONSE to
decide whether the target was actually vulnerable, and a WAF decides before a
response exists. So an imported rule is strictly weaker than the template it
came from — it recognises the attempt, not the exploitation. That is why they
land as action "log", which the engine now honours: observed, recorded as an
event, scoring nothing.

Also records what the importer skips rather than guesses: a CVE already
patched in compiled code, a request that is entirely template variables, a
literal under twelve characters, the site root. And what it corrects: one
leading path segment, because a template is written against a laboratory where
the application is mounted somewhere — /laravel52/vendor/phpunit/... would
have been published unable to match a real attack at /vendor/phpunit/...

Documentation only. cve.json is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5fpE6Gu4W6JoFMXyb7t7r
2026-11-04 02:34:54 +00:00

148 lines
6.3 KiB
Markdown

# The CVE channel (`cve.json`)
This document carries virtual patches: rules written against a named
vulnerability, published as data so that every edge node enforces them within
the hour instead of waiting for an engine release.
It is deliberately a second document in this repository, not a second
repository and not part of `rulesets.json`. Each has its own `build_datetime`,
so each is skipped or applied on its own, and a mistake in one cannot take out
the other.
Engine 3.0.47 and later fetch it hourly from:
https://git.artica.center/Andre/edgeguard-owasp-feed/raw/branch/main/cve.json
## Publishing a patch
Add a rule to the single ruleset, bump `build_datetime`, commit, push. An
unchanged `build_datetime` means every node skips the document without reading
it, so the stamp is what makes a publish take effect.
```json
{
"build_datetime": "2026-10-22T13:00:00Z",
"rulesets": [{
"id": "edgeguard-cve",
"name": "EdgeGuard CVE Virtual Patches",
"version": "1",
"source": "edgeguard-cve",
"description": "Virtual patches for named vulnerabilities",
"enabled": true,
"priority": 1,
"rules": [{
"id": "CVE-2025-31161-01",
"name": "CrushFTP authentication bypass via crafted Authorization header",
"pattern": "(?i)AWS4-HMAC-SHA256.*Credential=[^/]*/",
"targets": ["headers"],
"action": "block",
"severity": "critical",
"category": "cve",
"cve": "CVE-2025-31161",
"enabled": true,
"references": ["https://nvd.nist.gov/vuln/detail/CVE-2025-31161"]
}]
}]
}
```
That example is deliberately NOT the PHP upload-progress rule, which this
document used to show. That CVE is patched in compiled code, so the guard below
refuses it — the one example a reader was most likely to copy was the one
example that could not be published. Anything already patched in code belongs
in code; see the end of this page.
`source` must be `edgeguard-cve`. That is the source the channel owns and the
only one it may clear, which is also what makes a rule **withdrawable**: remove
it, bump the stamp, and it stops being enforced everywhere within the hour.
Publishing an empty `"rulesets": []` withdraws the lot.
## Importing from a Nuclei template
A CVE record describes a vulnerability, not a request, which is why this
channel is written by hand. A **Nuclei template** does contain the request —
path, query, headers, body — because that is what it sends. So for thousands of
CVEs the shape a virtual patch needs is already written down.
go run ./tools/nuclei-importer -cves CVE-2025-31161,CVE-2024-4577 \
-merge edgeguard-rules/cve.json > /tmp/next.json
cp /tmp/next.json edgeguard-rules/cve.json
bash tools/publish-cve-feed.sh --check
**Imported rules land as `action: "log"`, and that is not timidity.** A template
decides whether a target was actually vulnerable by reading the RESPONSE —
words in the body, the content type, the status. A WAF decides before a
response exists, so the imported rule is strictly weaker than the template it
came from: the template confirms exploitation, the rule only recognises the
attempt. Watch what it would have caught in your own traffic, then change
`log` to `block` deliberately, per rule.
Each imported rule carries an `origin` field saying a human did not write its
pattern and what it came from. Do not remove it — it is the difference between
a rule somebody reasoned about and one a converter guessed.
### What it refuses to do
It skips rather than guesses, and prints why:
- **A CVE the engine already patches in code.** Both rules would fire and the
scores would add. Five are patched today: the two PHP2Shell ones, Log4Shell
(two) and Spring4Shell.
- **A request with nothing literal in it.** Templates are full of
`{{BaseURL}}` and `{{string_1}}`; a pattern that is mostly `.*` matches
ordinary traffic.
- **A request whose longest literal is under twelve characters**, or is the
site root. A rule on `/` matches everything.
### What it corrects, and why you should check anyway
It strips one leading path segment when three remain, because a template is
written against a laboratory where the application is mounted somewhere: the
phpunit template requests `/laravel52/vendor/phpunit/...` and `laravel52` is
that lab's directory. Without stripping it the rule is published, enabled, and
unable to match a real attack at `/vendor/phpunit/...`.
It prefers a header value carrying an injection marker (`${jndi:`, `<%`, `$(`,
`../`) over a path, and ignores standard headers entirely whatever their value.
An earlier version preferred headers over everything and produced the pattern
`Content-Type: text/html` for a PHP RCE — a rule that says nothing about the
attack and something about every request.
None of that makes the output right. Read every pattern before publishing.
## Targets
The usual ones (`uri`, `query`, `args`, `body`, `headers`, `user_agent`,
`cookies`, `filename`, `all`) plus:
- **`param_names`** — the pattern is run against each parameter NAME on its
own: query parameters, urlencoded body fields and multipart
`Content-Disposition` names. An anchored pattern therefore anchors to one
name, and a parameter whose *value* happens to be the string can never match.
This is the shape CVE-2026-109618 needed and the reason it could not be
written against `args`, where names and values arrive as one blob and
`?note=PHP_SESSION_UPLOAD_PROGRESS` would have been refused.
Write `param_names` patterns in lower case; both the original and the
lowercased spelling of each name are tried.
## Severity is the score
`critical` 5, `high` 4, `medium` 3, `low` 2, against a default anomaly
threshold of 5. A patch that should refuse on its own is `critical`; one that
should only contribute is `medium` or below. Nothing else sets the score.
## Two things to get right
**Do not publish a rule for a CVE the engine already patches in code.** Both
would fire on the same request, the anomaly score would be the sum, and the
event would report one attack as two rules.
`TestPublishedFeedDoesNotDuplicateACompiledRule` in the engine fails if this
document names a CVE that `ruleCVEReferences` already covers.
**A patch written in the hours after a disclosure is written against an
advisory, not against traffic.** There is no backtest. If one turns out to
refuse something real, the remedy is to withdraw it — which is why withdrawal
is a first-class operation here.