Skip to content

An encoded NUL defeats the Iceberg encoded-traversal guard on object storage #844

Description

@jdatcmd

ice_has_encoded_dotdot exists to catch a traversal smuggled through percent-encoding, because an http(s) origin or proxy may decode a key before serving it. A %00 in front of the traversal defeats it.

Measured

Over the S3 fixture, pgcolumnar.iceberg_scan on a table whose equality-delete path is recorded as
file:///tmp/pgc_ice_del/db/t/%00%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/hostname:

recorded delete path SQLSTATE
../../../../etc/hostname (literal) 22023, refused
%2e%2e/%2e%2e/... (encoded) 22023, refused
%00%2e%2e/%2e%2e/... (encoded, behind an encoded NUL) 58P01

58P01 is undefined_file: the path was not refused. It passed both containment checks and was fetched, and the fetch failed at the escaped location.

Cause

ice_percent_decode_once decodes %00 to an embedded NUL and keeps writing. Everything downstream is NUL-terminated string work: the next pass measures with strlen, and ice_has_dotdot walks with strchr. Both stop at that byte and never reach the ../ behind it, so the function returns false. The literal-.. guard beside it sees no .. either, because the segments are encoded.

Where it matters

ice_rebase has two branches. For a local root it calls canonicalize_path and re-checks containment, which collapses the traversal regardless — so a local table is backstopped.

For a remote root the containment is purely lexical; the code says so:

Object storage: ... there is no realpath/symlink notion, so the containment is purely lexical

So this is the object-storage path, where the guard being defeated means there is no other check.

Not claimed

Whether a given origin or proxy would reconstruct a readable path from a key containing a NUL is not established here, and most reject NUL outright. What is established is that the guard fails to fire on the input its own comment says it exists to catch, and that the path is then fetched rather than refused.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions