Actions
Bug #8849
open
SB
JL
detect: DetectReferenceParse leaves ref->key NULL on unknown-key fallback path, leading to NULL deref in alert output
Bug #8849:
detect: DetectReferenceParse leaves ref->key NULL on unknown-key fallback path, leading to NULL deref in alert output
Affected Versions:
Effort:
Difficulty:
Label:
Description
Reported by Communications Security Establishment (CSE):
## Summary
When a rule's `reference:` keyword uses a key that is not defined in `reference.config` and strict keyword parsing is disabled (the default), `DetectReferenceParse()` takes a fallback branch that registers a synthetic reference entry but never assigns `ref->key` or `ref->key_len`. The resulting `DetectReference` is appended to the signature with `key == NULL`. Later, when an alert fires on that signature and EVE alert reference logging is enabled, `AlertJsonReference()` passes the NULL `kv->key` to `snprintf("%s%s", ...)`. This is undefined behaviour: on musl and several other libcs it is an immediate SIGSEGV (remote DoS on the first matching packet); on glibc it expands to the literal `(null)` and produces truncated, corrupted reference strings in `eve.json`.
## Affected Piece of Code
- **File:** `src/detect-reference.c`
- **Function / Location:** `DetectReferenceParse()` ~L161–188
- **Subsystem:** detect-parse — Rule signature parser and basic keyword parsers
```c
src/detect-reference.c (DetectReferenceParse):
163 SCRConfReference *lookup_ref_conf = SCRConfGetReference(key, de_ctx);
164 if (lookup_ref_conf != NULL) {
165 ref->key = SCStrdup(lookup_ref_conf->url);
...
170 ref->key_len = (uint16_t)strlen(ref->key);
171 } else {
172 if (SigMatchStrictEnabled(DETECT_REFERENCE)) {
173 SCLogError("unknown reference key \"%s\"", key);
174 goto error;
175 }
176
177 SCLogWarning("unknown reference key \"%s\"", key);
178
179 char str[2048];
180 snprintf(str, sizeof(str), "config reference: %s undefined\n", key);
181
182 if (SCRConfAddReference(de_ctx, str) < 0)
183 goto error;
184 lookup_ref_conf = SCRConfGetReference(key, de_ctx);
185 if (lookup_ref_conf == NULL)
186 goto error;
187 } /* <-- ref->key, ref->key_len NEVER ASSIGNED on this path */
188 }
src/output-json-alert.c (AlertJsonReference) — the consumer:
189 const size_t size_needed = kv->key_len + kv->reference_len + 3;
190 DEBUG_VALIDATE_BUG_ON(size_needed > DETECT_MAX_RULE_SIZE);
191 char kv_store[size_needed];
192 snprintf(kv_store, size_needed, "%s%s", kv->key, kv->reference);
```
## The Bug
`DetectReferenceParse()` builds a `DetectReference` from a `reference:<key>,<value>;` rule option. There are three mutually exclusive ways `ref->key` can be populated:
1. **Scheme override (L153–161):** if the parsed value begins with a `scheme://` prefix, `ref->key` is set from that scheme.
2. **Known key (L163–170):** if `SCRConfGetReference(key, de_ctx)` finds the key in the loaded `reference.config`, `ref->key = SCStrdup(lookup_ref_conf->url)` and `ref->key_len` are set.
3. **Unknown key, non-strict (L177–186):** the function logs a warning, synthesises a `config reference: <key> undefined` entry via `SCRConfAddReference()`, re-fetches it into `lookup_ref_conf`, verifies it is non-NULL — and then falls out of the `else` block.
Branch (3) is missing the assignment that branches (1) and (2) perform. After L186 succeeds, `lookup_ref_conf` points to a valid (synthetic) entry, but nothing copies `lookup_ref_conf->url` into `ref->key` or sets `ref->key_len`. Execution continues to L190 onward, `ref->reference` and `ref->reference_len` are populated from the value, and the half-initialised `DetectReference` (`key == NULL`, `key_len == 0`) is linked into `s->references` by `DetectReferenceSetup()` at L237–246.
The dangling NULL is dereferenced at alert time. `AlertJsonReference()` in `src/output-json-alert.c` walks `s->references` and, for each node, computes a VLA size of `kv->key_len + kv->reference_len + 3` and calls `snprintf(kv_store, size_needed, "%s%s", kv->key, kv->reference)`. With `kv->key == NULL`, passing NULL for a `%s` conversion is undefined behaviour per C99 §7.19.6.1:
- On **musl libc**, Solaris libc, and several BSD libcs, `printf("%s", NULL)` dereferences NULL and crashes with SIGSEGV.
- On **glibc**, NULL for `%s` is special-cased to the 6-byte string `(null)`. Because the VLA was sized using `key_len == 0`, the buffer is only `reference_len + 3` bytes; `snprintf` bounds the write so there is no overflow, but the emitted string is truncated (e.g. `"(null)001-"` for an 8-byte reference) and the JSON `references` array contains garbage instead of a URL.
In both cases the root cause is identical: the unknown-key branch forgot the `ref->key = SCStrdup(lookup_ref_conf->url)` assignment.
**Call chain — Phase 1 (state setup at engine init / rule reload; input is the local rule file):**
```
SigLoadSignatures() src/detect-engine-loader.c:384
→ DetectLoadSigFile() src/detect-engine-loader.c:121 (reads each rule line)
→ DetectEngineAppendSig() src/detect-parse.c:3595
→ SigInit() src/detect-parse.c:3248
→ SigInitHelper() src/detect-parse.c:3016
→ SigParse() src/detect-parse.c:1930
→ SigParseBasics() src/detect-parse.c:1807
→ SigParseOptions() src/detect-parse.c:871
→ st->Setup() src/detect-parse.c:1074
= sigmatch_table[DETECT_REFERENCE].Setup
= DetectReferenceSetup() src/detect-reference.c:224
→ DetectReferenceParse() src/detect-reference.c:100
```
With `rawstr = "unknownkey,001-2010"` the `PARSE_REGEX` yields `key = "unknownkey"`, `scheme = ""` (capture group 2 unmatched/empty), `uri = "001-2010"`. `strlen(scheme) == 0` so the L153 scheme branch is skipped. `SCRConfGetReference("unknownkey")` returns NULL because the key is absent from `reference.config`. `SigMatchStrictEnabled(DETECT_REFERENCE)` is false by default, so control enters L177–186: a dummy `config reference: unknownkey undefined` entry is registered, re-fetched into `lookup_ref_conf`, and the `else` block exits **without assigning `ref->key` or `ref->key_len`** (contrast L165–170). The resulting object — `key = NULL`, `key_len = 0`, `reference = "001-2010"`, `reference_len = 8` — is appended to `s->references` at L237–246.
**Call chain — Phase 2 (dereference at packet time; input is any matching network packet):**
```
packet capture → FlowWorker → detection engine matches the signature
→ PacketAlertAppend() stores PacketAlert with pa->s == this Signature
→ OutputLoggerLog() src/output.c:784
→ OutputPacketLog() src/output-packet.c:84
→ JsonAlertLogger() src/output-json-alert.c:910
gated by JsonAlertLogCondition() src/output-json-alert.c:922 (true: p->alerts.cnt > 0)
→ AlertJson() src/output-json-alert.c:694 (loops over alerts)
→ AlertJsonHeader() src/output-json-alert.c:752 → :206
because json_output_ctx->flags & LOG_JSON_REFERENCE
(set from config "metadata.rule.reference: true", parsed at :1028)
→ AlertJsonReference() src/output-json-alert.c:176
```
Inside `AlertJsonReference()`: `kv->key_len == 0`, `kv->reference_len == 8`, so `size_needed = 11`. `snprintf(kv_store, 11, "%s%s", kv->key /* NULL */, kv->reference)` at L192 passes NULL for `%s` — undefined behaviour. On musl/Solaris/some BSDs: SIGSEGV. On glibc: `(null)` (6 bytes) is emitted, the 11-byte buffer receives the truncated string `(null)001-`, and corrupted JSON is logged (no overflow — `snprintf` bounds it).
**Required field values to reach the bug:** the rule keyword must be `reference:<KEY>,<VAL>;` where `<KEY>` matches `[A-Za-z0-9]+` and is **not** defined in `reference.config`, and `<VAL>` matches `[a-zA-Z0-9\-_./?=]+` and does **not** begin with a scheme prefix `[a-zA-Z]+://` (otherwise the L153 scheme branch assigns `ref->key` and avoids the bug).
**Vulnerability class:** network-reachable NULL pointer dereference.
## Reproduction Results
**Preconditions (operator-side, non-default):**
1. `suricata.yaml` — enable EVE alert reference logging. This is off by default: `LOG_JSON_REFERENCE` is not part of `METADATA_DEFAULTS` at `src/output-json-alert.c:91-93`, and `suricata.yaml.in:206` ships the option commented out.
```yaml
outputs:
- eve-log:
enabled: yes
filetype: regular
filename: eve.json
types:
- alert:
metadata:
rule:
reference: true
```
2. Do **not** enable strict parsing for the `reference` keyword (this is the default). I.e. do not pass `--strict-rule-keywords=reference` and do not list `reference` under the `engine-analysis` strict keywords.
3. Use the stock `reference.config` (which has no entry for the key `unknownkey`).
4. Rule file `test.rules` — key not in `reference.config`, value has no `scheme://` prefix:
```
alert ip any any -> any any (msg:"refnull"; reference:unknownkey,001-2010; sid:1000001; rev:1;)
```
5. Start Suricata:
```
suricata -c suricata.yaml -S test.rules -r trigger.pcap
```
(or live on an interface). Startup will print `Warning: detect-reference: unknown reference key "unknownkey"` — confirming the vulnerable branch was taken.
**Network trigger (attacker-supplied):**
6. Send or replay **any** IPv4 packet that matches `ip any any -> any any`. A minimal raw frame for a pcap (Ethernet + IPv4 + ICMP echo, 42 bytes total):
```
ff ff ff ff ff ff 00 11 22 33 44 55 08 00 # Ethernet, ethertype 0x0800
45 00 00 1c 00 01 00 00 40 01 66 d2 0a 00 00 01 0a 00 00 02 # IPv4 hdr, proto=1, 10.0.0.1→10.0.0.2
08 00 f7 fd 00 01 00 01 # ICMP type 8 code 0
```
Wrap this single frame in a pcap and feed via `-r`, or send it on the monitored interface.
**Observed result:**
- **musl libc / non-glibc:** SIGSEGV inside `snprintf()` called from `AlertJsonReference()` (`src/output-json-alert.c:192`) — the Suricata process crashes on the first matching packet.
- **glibc:** no crash; `eve.json` contains an alert record with `"references":["(null)001-"]` (truncated, because the VLA was sized `0 + 8 + 3 = 11` and `(null)` consumed 6 of those bytes) — corrupted log output and detection-bypass for downstream consumers expecting a valid URL.
**Reproduction status:** analytical only — the trigger was not executed because task constraints forbid building/running Suricata. All call-chain edges and field values were verified statically in the source; no blocker prevents construction of a working trigger exactly as described above.
## Severity
**LOW**
On musl libc (Alpine-based containers) and other libcs that do not special-case NULL for `%s`, this is a remote crash / DoS: the first packet matching the affected signature after engine start kills the IDS/IPS process. On glibc there is no crash, but the EVE alert `references` array contains truncated garbage such as `"(null)001-"` instead of the intended URL, breaking downstream SIEM correlation — a log-correctness / minor detection-bypass issue. There is no memory corruption, since `snprintf` bounds the write to the VLA.
Exploitation requires the **non-default** config option `eve-log.alert.metadata.rule.reference: true` **and** a rule file (semi-trusted, often pulled from third-party rule feeds) containing a `reference:` key that is absent from the local `reference.config`. The latter condition occurs routinely in practice with community/ET rules that reference vendor-specific systems, but the combination with the non-default EVE flag keeps overall exposure limited.
## Suggested Fix
**Primary fix** — assign `ref->key` in the unknown-key fallback branch exactly as the known-key branch does:
```diff
--- a/src/detect-reference.c
+++ b/src/detect-reference.c
@@ -183,7 +183,12 @@ static DetectReference *DetectReferenceParse(const char *rawstr, DetectEngineCt
goto error;
lookup_ref_conf = SCRConfGetReference(key, de_ctx);
if (lookup_ref_conf == NULL)
goto error;
+ ref->key = SCStrdup(lookup_ref_conf->url);
+ if (ref->key == NULL) {
+ goto error;
+ }
+ ref->key_len = (uint16_t)strlen(ref->key);
}
}
```
**Defence-in-depth** — also harden the consumer in `src/output-json-alert.c` `AlertJsonReference()`:
```diff
- const size_t size_needed = kv->key_len + kv->reference_len + 3;
+ const char *k = kv->key ? kv->key : "";
+ const char *r = kv->reference ? kv->reference : "";
+ const size_t size_needed = strlen(k) + strlen(r) + 3;
DEBUG_VALIDATE_BUG_ON(size_needed > DETECT_MAX_RULE_SIZE);
char kv_store[size_needed];
- snprintf(kv_store, size_needed, "%s%s", kv->key, kv->reference);
+ snprintf(kv_store, size_needed, "%s%s", k, r);
```
Optionally, extend `DetectReferenceParseTest03` to assert `s->references->key != NULL` after parsing the `unknownkey` rule, so a regression on this path fails the unit-test suite.
VJ Updated by Victor Julien 9 days ago
- Subject changed from DetectReferenceParse leaves ref->key NULL on unknown-key fallback path, leading to NULL deref in alert output to detect: DetectReferenceParse leaves ref->key NULL on unknown-key fallback path, leading to NULL deref in alert output
- Status changed from New to Assigned
- Assignee changed from OISF Dev to Jeff Lucovsky
VJ Updated by Victor Julien 2 days ago
- Tracker changed from Security to Bug
- Private changed from Yes to No
Treating like a regular bug as it's quite an unlikely and non-default scenario.
Actions