Skip to content

Spurious nested application/zip match on the end of central directory magic field #3470

Closed

Description

@ESultanik

Summary

Parsing any ZIP archive produces a spurious nested application/zip match on the four-byte magic
field of the end of central directory record, along with a parser warning. The nested match is a
false positive: the four bytes PK\x05\x06 are a field of the record that was already matched, not
an embedded archive.

Reproducer

python -c "import zipfile; zipfile.ZipFile('a.zip','w').writestr('a.txt', b'hello\n')"
polyfile --debug --format sbud a.zip

Actual result

On stderr:

Parser parse_zip for MIME type application/zip raised an exception while parsing
Zip archive data (empty) in /tmp/tmp0o2w_3pd: Reached the end of stream while
expecting 2-byte unsigned integer for EndOfCentralDirectory.disk_number

And in the SBUD tree:

application/zip
  EndOfCentralDirectory
    magic @92 +4
      application/zip @92 +4     <- false positive

Expected result

No nested application/zip match on the magic field, and no parser warning.

Cause

PolyFileStruct.match in polyfile/structmatcher.py:42-44 writes every bytes field to a
temporary file and runs the full matcher over it, to find embedded files. The relaxed ZIP test in
polyfile/zipmatcher.py:15-20 searches for PK\x05\x06 anywhere in a file, so a four-byte file
containing exactly that sequence matches as an empty ZIP. parse_zip then runs on those four bytes
and fails, because there is nothing after the signature to read.

The same pattern applies to any struct whose magic field happens to satisfy a matcher, so raising
the minimum size for the relaxed ZIP test, or skipping recursion into Constant fields, would both
address it.

Version

PolyFile 0.5.6, at ffdf476. This is unrelated to #3364; it reproduces on an ordinary archive that
starts at offset 0.

Activity

  1. self-assigned this
    on Sep 11, 2026
  2. added this to the v0.6.0 milestone on Sep 11, 2026
  3. ESultanik commented on Sep 11, 2026

    @ESultanik
    CollaboratorAuthor

    Anchor confirmation, and the precise mechanism

    Verified against master at 906d916. The cause in the issue body is right; here is the exact
    chain.

    PolyFileStruct.match at polyfile/structmatcher.py:18-46 recurses at :39-46:

    40      if isinstance(value, PolyFileStruct):
    41          yield from value.match(matcher, s)
    42      elif isinstance(value, bytes):
    43          with Tempfile(value) as tmp:
    44              yield from matcher.match(tmp, parent=s)

    Constant is a bytes subclass. class Constant(ByteField) is at polyfile/structs.py:210-224
    and ByteField(bytes, Field, ...) at :159. So EndOfCentralDirectory.magic
    (polyfile/zipmatcher.py:124, the four bytes PK\x05\x06) hits the :42 branch unconditionally,
    gets written to a temp file, and is re-matched. The relaxed ZIP test at zipmatcher.py:15-20 is a
    bare search for that signature anywhere in a file, so a four-byte file containing exactly it
    matches as an empty ZIP. parse_zip then fails on it, which is the warning.

    Either fix named in the issue works: an isinstance(value, Constant) guard at :42, or a minimum
    size for the relaxed test. The Constant guard is the more general of the two — the issue is right
    that any struct whose magic field satisfies a matcher shows the same symptom, not just ZIP.

    The three entry points that trigger it are parse_zip at polyfile/zipmatcher.py:194-224:
    fh.match(...) at :210, cd.match(...) at :223, eocd.match(...) at :224.

    Sequencing

    This blocks #3530. structmatcher.py:44 calls Matcher.match, which #3466 rewrites, so this
    guard has to land first or the two changes disagree about how many submatches that line yields.

    Verification

    The issue's reproducer is the acceptance test:

    python -c "import zipfile; zipfile.ZipFile('a.zip','w').writestr('a.txt', b'hello\n')"
    polyfile --debug --format sbud a.zip

    Assert no nested application/zip on the magic field and no parser warning on stderr.
    tests/test_zipmatcher.py is the home for it. Check whether any other bundled struct has a
    Constant field that a matcher accepts, and cover the general case rather than only ZIP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions