Repository navigation
DROP 1.2.0
⚠️ Correction — see 1.2.1The security section below says the static-app dotfile exposure is "not
fully closed by this change" and names Caddy'sfile_serveras the
component at fault. The attribution is wrong, and the exposure is now
fixed.Static apps are not served by Caddy's
file_server— that branch is dead
code, becausestaticPathis never assigned. They run nginx in their own
container, from a config DROP generates; Caddy only reverse-proxies to it.
The exposure itself was real: for a plain-root static app the document
root is the app directory, so/.git/configand/.envwere served.
An SPA was never affected.Fixed in 1.2.1.
Upgrading is not enough on its own — restart your static apps, which
regenerates their nginx config. If/.git/configreturned 200 on an app
that was git-deployed before 1.2.0, rotate the personal access token it
contained.
[1.2.0] - 2026-08-09
Two security fixes that change behaviour, and the recovery path for a repo
that goes private after it was deployed.
Read before upgrading — two compatibility breaks, both security-driven:
- An upload carrying
.gitis now refused.tar -czf app.tgz .from a
working tree fails instead of silently deploying the repository's git
metadata. Exclude it (--exclude .git), or deploy viaPOST /git/deploy. gitSource.tokenIdis no longer returned below the admin tier, and
gitSource.repoUrlis normalized. Breaking for any script or agent that
readtokenIdfromGET /api/v1/apps/:name.
Security
-
Uploaded archives containing
.gitmetadata are now rejected. An
archive with a.gitpath component — at any depth, case-insensitively —
is refused withreason: vcs_metadataand nothing is extracted. Previously
a tenant with theuserrole could upload a crafted.git/to
POST /apps/<app>/source, where it overwrote the app's real one, and a
subsequentPOST /git/redeploy/<app>rangit pullin that directory on
the host — never containerized in either isolation mode — so a poisoned
.git/config(anext::sh -c …remote URL,core.fsmonitor) executed
arbitrary commands as thedropuser, which is in thedockergroup and
therefore root-equivalent.The guard reads the parser's resolved entry path rather than the raw tar
header name, because a PAX extended header can override the path of the
entry that follows it — a check against the header name would have closed
nothing.Behaviour change for hand-rolled clients:
tar -czf app.tgz .from a
working tree now fails instead of silently deploying the repository's git
metadata. Exclude it (--exclude .git), or usePOST /git/deployto
deploy a repository. The MCPdeploy_filestool rejects such paths before
staging. -
Dotenv files are excluded from a dashboard folder upload.
.env,
.env.local,.env.productionand the like are skipped (templates —
.env.example,.env.sample,.env.template,.env.dist— still ship),
and the upload panel lists exactly what it left out. For a static app the
uploaded tree root is the web server's document root, so a shipped.env
was fetchable at/.envon the public URL. Use the secrets API
(PUT /api/v1/secrets/<app>) for values the app needs at runtime. -
Personal access tokens no longer reach disk on either git path
(DROP-142).git clone https://TOKEN@github.com/…recorded the URL
verbatim asremote.origin.url, so every app deployed from a private repo
carried its PAT in cleartext inside its own directory — which is the served
document root for a static app and is bind-mounted into the tenant's
container under docker isolation.gitPullwrote the same value for the
duration of a pull. Both now pass the token through a one-shot credential
helper that reads it from the git child's own environment (never argv,
which is world-readable viaps), scoped tohttps://github.comso a
tampered remote URL cannot redirect the credential to another host. -
Existing repositories are cleaned up on their next redeploy (DROP-142).
Closing the leak above does nothing for apps already on disk, and git
prefers a credential embedded in the remote URL — so on exactly those
apps the new helper would never fire. A redeploy now strips the userinfo
fromremote.origin.urlbefore pulling. Operator note: an app that is
never redeployed keeps the old value; grep for it with
sudo grep -hE '^\s*url\s*=' /var/drop/data/webapps/*/.git/config | grep '@'.Not fully closed by this change: Caddy's
file_serverhas nohide
directive, so a plain-root static app still serves/.git/config— and its
history — to the internet.
Added
- A git credential can be attached to an app that already exists
(DROP-142).POST /api/v1/git/redeploy/:nametakes an optional
{ "tokenId": … }: absent leaves the app's stored credential unchanged,
nullclears it, agit_…id attaches or replaces one. Answers "a repo
that was public and went private can no longer be updated" — the token was
always stored by reference and re-read at redeploy, but nothing could write
that reference after creation. The dashboard exposes it as a credential
picker next to the existing Redeploy button on an app's detail page.
Changed
gitSourceis narrower for non-admin API consumers (DROP-142).
GET /api/v1/apps/:nameno longer returnsgitSource.tokenIdbelow the
admin tier, andgitSource.repoUrlhas any userinfo stripped. The
gitSourcefield itself stays present at every tier. Potentially breaking
for a script or agent that readtokenIdfrom an app record.
Fixed
-
An archive whose entries the tar parser rejects no longer deploys as a
partial tree. node-tar runs non-strict here, so an entry with a bad
checksum was silently dropped while extraction still reported success — and
the destination was then pruned to match, deleting files that had gone
missing. Any parser warning is now fatal (reason: invalid_archive), except
the one node-tar emits for an archive with no entries at all, which still
reportsempty_archive.This closes the cases the parser reports. A tar stream truncated mid-way
inside an otherwise-valid gzip wrapper is still extracted up to the
truncation point, because node-tar signals nothing at all in that case —
measured, and not something a warning-based check can reach. -
Clearing a git credential actually clears it (DROP-142). A clear is
persisted before the pull rather than after it — otherwise the now
unauthenticated pull fails against a private repo and the clear is
discarded, leaving no way to detach a compromised token. -
Git operations are pinned to the app's own repository (DROP-142).
They ran with only a working directory, so an app whose.githad been
removed — by an upload deploy's prune, or a monorepo re-materialization —
resolved to whatever repository existed above it and reported that
repo's commit as the app's own. -
Container CPU is no longer under-reported by the host core count
(DROP-143). The core count was derived frompercpu_usage, a cgroup
v1-only field. Under cgroup v2 — the default on current Debian and Ubuntu —
Docker omits it and reportsonline_cpusinstead, so the divisor fell back
to 1 and every reading on the dashboard was the true figure divided by the
number of host cores. -
"Back to home" on the login and signup pages works again (DROP-145).
It linked to/, which since the site split resolves to the dashboard's
own host, redirects to/dashboard, and sends a logged-out visitor
straight back to/login— a closed loop. It now points at the marketing
host, and renders nothing at all on a single-host install, which has no
landing page to return to.