Summary
build_store_data.py zips every file under an app folder with no filter, so anything sitting in the working tree at build time is shipped inside <app>.zip — including __pycache__/*.pyc left behind by the update checker.
The code
for p in app_path.glob('**/*'):
if p.is_dir():
continue
if p.name == zip_file.name:
continue
z.write(p, p.relative_to(app_path))
Two exclusions: directories, and the zip being written. Everything else is included.
Reproduction
update/update.py loads each app's checker via importlib (spec_from_file_location → exec_module, lines 30–32), which writes apps/<app>/__pycache__/update_check.cpython-*.pyc. This checkout currently has 40 such directories.
Running the documented update sequence in one working tree — update.py check, then update.py apply, then build_store_data — therefore produces contaminated zips. Demonstrated against the current tree:
files build_store_data would put in paperless-ngx.zip:
__pycache__/update_check.cpython-313.pyc <-- junk
app_meta.json
docker-compose.yml.template
icon.svg
update_check.py
junk entries: 1 of 5
One in five files in that app's zip is a stale bytecode artifact.
Why it matters, and why it is not urgent
The zips are what actually ship: deploy.yml uploads apps/ to blob storage, and shards install by fetching <app>/<app>.zip. So a contaminated zip reaches real installs.
That said, the deploy and preview CI jobs check out fresh and run only build_store_data.py, so they are clean today. The exposure is local builds and any future job that runs the update checker and the zip builder in the same workspace — which is exactly the sequence the update pipeline documents. So this is currently latent rather than actively broken, and worth fixing before something reorders those steps.
Nothing here is a security problem: a .pyc of a file already in the zip leaks nothing. It is correctness and hygiene — installs carry bytecode compiled for whatever Python happened to be on the builder's machine (cpython-313 here), which is neither used nor meaningful on the shard.
Suggested fix
Filter the glob. Something like an explicit deny for __pycache__, *.pyc, .DS_Store and dotfiles, or better, an allow-list of what an app zip is actually supposed to contain — app_meta.json, docker-compose.yml.template, the icon, update_check.py and any template assets. An allow-list also makes the zip's contents a deliberate decision rather than whatever the directory happens to hold.
A check with teeth
A test that asserts a built zip contains only expected entries would fail against today's tree, because the __pycache__ dirs are already there. A test that merely asserts the expected files are present would pass with the bug intact, so it must assert the absence of everything else.
Found independently by two agents while migrating mosquitto and filebrowser off the private registry (PRs #61 and #62); neither fixed it, since it affects every app rather than theirs. Verified separately against the current tree before filing.
Summary
build_store_data.pyzips every file under an app folder with no filter, so anything sitting in the working tree at build time is shipped inside<app>.zip— including__pycache__/*.pycleft behind by the update checker.The code
Two exclusions: directories, and the zip being written. Everything else is included.
Reproduction
update/update.pyloads each app's checker viaimportlib(spec_from_file_location→exec_module, lines 30–32), which writesapps/<app>/__pycache__/update_check.cpython-*.pyc. This checkout currently has 40 such directories.Running the documented update sequence in one working tree —
update.py check, thenupdate.py apply, thenbuild_store_data— therefore produces contaminated zips. Demonstrated against the current tree:One in five files in that app's zip is a stale bytecode artifact.
Why it matters, and why it is not urgent
The zips are what actually ship:
deploy.ymluploadsapps/to blob storage, and shards install by fetching<app>/<app>.zip. So a contaminated zip reaches real installs.That said, the deploy and preview CI jobs check out fresh and run only
build_store_data.py, so they are clean today. The exposure is local builds and any future job that runs the update checker and the zip builder in the same workspace — which is exactly the sequence the update pipeline documents. So this is currently latent rather than actively broken, and worth fixing before something reorders those steps.Nothing here is a security problem: a
.pycof a file already in the zip leaks nothing. It is correctness and hygiene — installs carry bytecode compiled for whatever Python happened to be on the builder's machine (cpython-313here), which is neither used nor meaningful on the shard.Suggested fix
Filter the glob. Something like an explicit deny for
__pycache__,*.pyc,.DS_Storeand dotfiles, or better, an allow-list of what an app zip is actually supposed to contain —app_meta.json,docker-compose.yml.template, the icon,update_check.pyand any template assets. An allow-list also makes the zip's contents a deliberate decision rather than whatever the directory happens to hold.A check with teeth
A test that asserts a built zip contains only expected entries would fail against today's tree, because the
__pycache__dirs are already there. A test that merely asserts the expected files are present would pass with the bug intact, so it must assert the absence of everything else.Found independently by two agents while migrating
mosquittoandfilebrowseroff the private registry (PRs #61 and #62); neither fixed it, since it affects every app rather than theirs. Verified separately against the current tree before filing.