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
148 lines
6.3 KiB
Markdown
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.
|