Summary
mk_relative_path_store, KeyCodecs.prefixed and prefixless_view all relativize keys by unguarded slicing. Keys that do not start with the prefix are not filtered out — they are silently corrupted into plausible-looking keys that address a different object, and which are writable.
Found while redesigning s3dol (i2mint/s3dol), where a prefix is used as a tenant/app boundary. Verified against dol 0.3.58.
Repro
from dol import KeyCodecs, Pipe, filt_iter, mk_relative_path_store
base = {'a/b': 1, 'a/c': 2, 'z': 3, 'ab/x': 4}
sorted(KeyCodecs.prefixed('a/')(dict(base))) # ['', '/x', 'b', 'c'] <- CORRUPT
# mk_relative_path_store(cls, prefix_attr='prefix') # ['', '/x', 'b', 'c'] <- CORRUPT
# prefixless_view(store, prefix='a/') # ['', '/x', 'b', 'c'] <- CORRUPT
sorted(Pipe(filt_iter.prefixes('a/'), KeyCodecs.prefixed('a/'))(dict(base))) # ['b', 'c'] OK
Two distinct corruptions:
- the non-matching key
z becomes '' — and w[''] then raises KeyError: 'a/'
- the sibling key
ab/x becomes /x
Why it matters
For a store scoped to logs/, a neighbouring object logs2/2026.txt surfaces as the key 2/2026.txt. It reads, and it writes — so a store scoped to one prefix can silently read and overwrite objects under another. Wherever a prefix is used as an isolation boundary (per-tenant, per-app, per-environment), that is a boundary violation produced by the storage layer itself, with no error anywhere.
It also makes the natural fix for a hand-rolled id[len(prefix):] not a fix — s3dol had exactly that bug and "use dol's canonical mechanism" would have reproduced it under a clean-architecture label.
Suggested fix
A strict=True mode on the prefix machinery (candidate default in a future major) where a key outside the prefix raises rather than being sliced, plus a property test:
∀ k in-scope: key_of_id(id_of_key(k)) == k
∀ i out-of-scope: key_of_id(i) raises # never returns a corrupted key
Until then, the only safe composition is Pipe(filt_iter.prefixes(p), KeyCodecs.prefixed(p)) — i.e. filt_iter.prefixes is a correctness requirement, not an optimization — and it is worth documenting that prominently, because the ordering is not obvious and reversing it yields a silently empty store.
Prefix normalization matters too: with prefix='logs' (no trailing delimiter) even the safe composition exposes logs2/.... Normalizing to prefix.strip(sep) + sep should probably happen inside the machinery rather than being left to each caller.
Related, same area
filter_prefixes regex grouping. dol.trans.filter_prefixes(['logs/', 'tmp/']) compiles to ^logs/|tmp/, which parses as (^logs/)|(tmp/) — so 'zzz/tmp/c' matches. Needs ^(?:logs/|tmp/).
_filt_iter assigns __len__ unconditionally (dol/trans.py, no hasattr guard), so wrapping a class that deliberately omits __len__ (because counting means unbounded paginated listing over a remote backend) silently resurrects it — and restores the double-listing that omitting it was meant to avoid, since list() takes a length hint from __len__.
Happy to send a PR for any/all of these.
Summary
mk_relative_path_store,KeyCodecs.prefixedandprefixless_viewall relativize keys by unguarded slicing. Keys that do not start with the prefix are not filtered out — they are silently corrupted into plausible-looking keys that address a different object, and which are writable.Found while redesigning
s3dol(i2mint/s3dol), where a prefix is used as a tenant/app boundary. Verified against dol 0.3.58.Repro
Two distinct corruptions:
zbecomes''— andw['']then raisesKeyError: 'a/'ab/xbecomes/xWhy it matters
For a store scoped to
logs/, a neighbouring objectlogs2/2026.txtsurfaces as the key2/2026.txt. It reads, and it writes — so a store scoped to one prefix can silently read and overwrite objects under another. Wherever a prefix is used as an isolation boundary (per-tenant, per-app, per-environment), that is a boundary violation produced by the storage layer itself, with no error anywhere.It also makes the natural fix for a hand-rolled
id[len(prefix):]not a fix — s3dol had exactly that bug and "use dol's canonical mechanism" would have reproduced it under a clean-architecture label.Suggested fix
A
strict=Truemode on the prefix machinery (candidate default in a future major) where a key outside the prefix raises rather than being sliced, plus a property test:Until then, the only safe composition is
Pipe(filt_iter.prefixes(p), KeyCodecs.prefixed(p))— i.e.filt_iter.prefixesis a correctness requirement, not an optimization — and it is worth documenting that prominently, because the ordering is not obvious and reversing it yields a silently empty store.Prefix normalization matters too: with
prefix='logs'(no trailing delimiter) even the safe composition exposeslogs2/.... Normalizing toprefix.strip(sep) + sepshould probably happen inside the machinery rather than being left to each caller.Related, same area
filter_prefixesregex grouping.dol.trans.filter_prefixes(['logs/', 'tmp/'])compiles to^logs/|tmp/, which parses as(^logs/)|(tmp/)— so'zzz/tmp/c'matches. Needs^(?:logs/|tmp/)._filt_iterassigns__len__unconditionally (dol/trans.py, nohasattrguard), so wrapping a class that deliberately omits__len__(because counting means unbounded paginated listing over a remote backend) silently resurrects it — and restores the double-listing that omitting it was meant to avoid, sincelist()takes a length hint from__len__.Happy to send a PR for any/all of these.