ruleset_feeds.cve_url points an installation at its own channel document, from engine 3.2.7. It replaces the published channel rather than adding to it: the channel owns its ruleset source, a fetch clears that source before rebuilding it, and two feeds claiming it would delete each other's rules on alternating ticks. So a new patch can be published to your own document, run as log against your own traffic for as long as it takes to trust, and only then proposed here. Until now there was nowhere to do that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N5fpE6Gu4W6JoFMXyb7t7r
197 lines
8.8 KiB
Markdown
197 lines
8.8 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
|
|
|
|
## Rehearsing a rule before it reaches anybody
|
|
|
|
Since engine 3.2.7 an installation can point the channel at its own document:
|
|
|
|
```json
|
|
{ "ruleset_feeds": { "enabled": true, "cve_url": "https://your.host/cve.json" } }
|
|
```
|
|
|
|
It **replaces** the published channel rather than adding to it, because the
|
|
channel owns its ruleset source: a fetch clears that source before rebuilding
|
|
it, which is what makes a rule withdrawable, and two feeds claiming it would
|
|
delete each other's rules on alternating ticks. `rulesets_url` does the same
|
|
for the CRS mirror. Leave both empty for the published documents, which is the
|
|
right default — an installation that sets nothing gets the patches we publish.
|
|
|
|
So the sequence for a new patch is: publish it to your own document, let it run
|
|
as `log` against your own traffic for as long as it takes to trust, and only
|
|
then propose it here. Until this existed there was nowhere to do that, and
|
|
every publish was a decision taken on every customer's behalf with no
|
|
rehearsal.
|
|
|
|
## When a rule belongs here, and when it does not
|
|
|
|
This document reaches every customer's agents from one hardcoded URL, so a
|
|
publish is a decision taken on their behalf. It earns that only in a narrow
|
|
case: **an actively exploited vulnerability, recognisable in a single request,
|
|
in software customers run, during the days before a real fix exists.** That is
|
|
a few a year.
|
|
|
|
Entry and exit, so rules do not accumulate because publishing is easy:
|
|
|
|
1. **In as `log`.** It records matches and scores nothing, so it refuses
|
|
nothing while you see what it would have caught.
|
|
2. **Promoted to `critical` only with a reason.** Severity is what acts —
|
|
`high` scores 4 against a default threshold of 5 and does nothing on its
|
|
own. A rule left at `high` is inert, not cautious.
|
|
3. **Then out.** Anything still worth enforcing after the emergency moves into
|
|
the engine, where it works with no network and cannot be withdrawn by a bad
|
|
publish — Log4Shell, Spring4Shell and PHP2Shell all live there. Anything no
|
|
longer worth it is deleted and the stamp bumped.
|
|
|
|
A rule sitting here indefinitely is in the wrong place.
|
|
|
|
**And customers do not need this channel for their own rules.** Create Custom
|
|
Ruleset in the console takes the same fields and reaches their agents through
|
|
the configuration pull, scoped to the sites they choose. The config pull is
|
|
forbidden from touching the sources this feed owns and this feed is forbidden
|
|
from touching theirs, so the two cannot clear each other.
|
|
|
|
## 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.
|