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.
ice_has_encoded_dotdotexists to catch a traversal smuggled through percent-encoding, because an http(s) origin or proxy may decode a key before serving it. A%00in front of the traversal defeats it.Measured
Over the S3 fixture,
pgcolumnar.iceberg_scanon a table whose equality-delete path is recorded asfile:///tmp/pgc_ice_del/db/t/%00%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/hostname:../../../../etc/hostname(literal)%2e%2e/%2e%2e/...(encoded)%00%2e%2e/%2e%2e/...(encoded, behind an encoded NUL)58P01isundefined_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_oncedecodes%00to an embedded NUL and keeps writing. Everything downstream is NUL-terminated string work: the next pass measures withstrlen, andice_has_dotdotwalks withstrchr. 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_rebasehas two branches. For a local root it callscanonicalize_pathand 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:
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.