google / google/clusterfuzz

Fuzzer metadata injection via decode_to_unicode errors='ignore' + splitlines() line separator abuse

Open
#5,428 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
5.6k
Forks
629
Avg merge
4d 6h
Merged PRs (30d)
22

Description

## Summary

Two weaknesses in ClusterFuzz's fuzzer output parsing pipeline can be chained to inject arbitrary `issue_labels` and `issue_components` into Buganizer when a fuzzer runs on OSS-Fuzz infrastructure. The result is silent misrouting of security bug reports — including those for OT0 projects like Golang, Flutter, and Angular.

---

## Vulnerable Code

**1. `src/clusterfuzz/_internal/base/utils.py:106`**
```python
def decode_to_unicode(obj):
return obj.decode('utf-8', errors='ignore') # invalid bytes silently dropped
```

**2. `src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py:83,760-766`**
```python
FUZZER_METADATA_REGEX = re.compile(r'metadata::(\w+):\s*(.*)')

def get_fuzzer_metadata_from_output(fuzzer_output):
metadata = {}
for line in fuzzer_output.splitlines(): # \r \x85 \u2028 \u2029 all split here
match = FUZZER_METADATA_REGEX.match(line)
if match:
metadata[match.group(1)] = match.group(2)
return metadata
```

**3. `src/clusterfuzz/_internal/issue_management/issue_filer.py:470-476`**
```python
metadata_components = _get_from_metadata(testcase, 'issue_components')
if issue_tracker.project == 'google-buganizer' and metadata_components:
issue.components.clear() # automatic component wiped
for component in metadata_components:
issue.components.add(component)
```

---

## Attack Vectors

### Vector 1 — CR Injection
```python
b"metadata::issue_labels: Type-Bug-Security\rmetadata::issue_components: 1234567"
```
`splitlines()` splits on `\r` → two separate metadata keys from one "line".

### Vector 2 — Invalid UTF-8 Byte Dropping
```python
b"metadata::issu\xc3e_labels: Restrict-View-SecurityTeam"
```
`errors='ignore'` drops `\xc3` silently → key becomes `issue_labels`.

### Vector 3 — Combined (obfuscated + multi-key)
```python
b"metadata::issu\xc3e_labels: Restrict-View-SecurityTeam\rmetadata::issu\xc3e_components: 1234567"
```
Invalid bytes obfuscate both keys in raw logs; parser produces two clean keys.

### Vector 4 — Unicode Line Separators
```
U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR), U+0085 (NEL)
```
All treated as line endings by `splitlines()`. Invisible in most log viewers.

---

## Steps to Reproduce

1. Submit a project to OSS-Fuzz with a fuzz target whose stdout outputs any of the above payloads alongside normal libFuzzer output.
2. ClusterFuzz bot runs the fuzzer and captures stdout.
3. `get_fuzzer_metadata_from_output()` parses the output — injected keys extracted.
4. `issue_filer.py` clears the automatic Buganizer component and sets attacker-controlled value.

Minimal reproduction:

```python
import re

FUZZER_METADATA_REGEX = re.compile(r'metadata::(\w+):\s*(.*)')

def decode_to_unicode(data):
return data.decode('utf-8', errors='ignore') # utils.py:106

def get_fuzzer_metadata_from_output(fuzzer_output):
metadata = {}
for line in fuzzer_output.splitlines():
match = FUZZER_METADATA_REGEX.match(line)
if match:
metadata[match.group(1)] = match.group(2)
return metadata

# CR injection: one payload line → two metadata keys
payload = b"metadata::issue_labels: Type-Bug-Security\rmetadata::issue_components: 1234567"
print(get_fuzzer_metadata_from_output(decode_to_unicode(payload)))
# {'issue_labels': 'Type-Bug-Security', 'issue_components': '1234567'}

# Invalid UTF-8 byte dropping: \xc3 silently removed → valid key
payload2 = b"metadata::issu\xc3e_labels: Restrict-View-SecurityTeam"
print(get_fuzzer_metadata_from_output(decode_to_unicode(payload2)))
# {'issue_labels': 'Restrict-View-SecurityTeam'}
```

---

## Fix Suggestion

```python
# utils.py — don't silently drop bytes
return obj.decode('utf-8', errors='replace')

# fuzz_task.py — sanitize before splitlines
fuzzer_output = fuzzer_output.replace('\r', '').replace('\x85', '').replace('\u2028', '').replace('\u2029', '')

# issue_filer.py — validate component before clearing
if metadata_components and all(c.isdigit() for c in metadata_components):
issue.components.clear()
```

Contributor guide

Open the contributing guide

Research direction

Start with decode_to_unicode in src/clusterfuzz/_internal/base/utils.py, then trace get_fuzzer_metadata_from_output in fuzz_task.py and metadata handling in issue_filer.py. Run the minimal reproductions from the issue and inspect existing tests around these entry points. Done means malformed bytes and alternate line separators cannot create metadata keys that let fuzzer output control Buganizer labels or components.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.