Skip to content

feat: PUT /file/<absolute-path> -- scriptable file-write route - #7

Merged
cablehead merged 2 commits into
mainfrom
feat/filedrop
Jul 2, 2026
Merged

feat: PUT /file/<absolute-path> -- scriptable file-write route#7
cablehead merged 2 commits into
mainfrom
feat/filedrop

Conversation

@cablehead

Copy link
Copy Markdown
Owner

Adds one route: PUT /file/<absolute-path> writes the request body to that path, mkdir -p'ing the parent first. No new capability -- a pty on this connection can already write anywhere as the server's user (sudo included). This is a clean one-shot call for a script to do the same thing, instead of driving a terminal (shell-escaping/base64-ing a body through a VT100 stream, then scraping rendered output to infer success).

Deliberately no configurable root and no path sandboxing -- there's no trust boundary here to protect that the terminal doesn't already cross, so restricting it would be friction without protection.

Also documents a real gap I hit building this: --dev doesn't work on an installed/downloaded release binary. $CARGO_MANIFEST_DIR (the path --dev reads) is a Rust compile-time constant baked in wherever the binary was built -- for a release build that's GitHub Actions' checkout path, which doesn't exist anywhere else. Confirmed empirically:

$ stacks2099 --dev 127.0.0.1:9999 --store /tmp/x
Error reading /home/runner/work/stacks2099/stacks2099/app/serve.nu: No such file or directory (os error 2)

Tested locally: valid write round-trips correctly, overwrite works, bad/empty paths 400, wrong method falls through cleanly. Built from this checkout with cargo build --release, cargo check/build clean.

Same reach a pty on this connection already has (arbitrary write as the
server's user, sudo included) -- this isn't a new capability, just a clean
one-shot call for a script instead of driving a terminal to do the same
thing (shell-escaping/base64 a binary body through a VT100 stream to write
a file, then scraping rendered output to infer success).

The URL suffix after /file is used directly as the destination path (e.g.
PUT /file/run/secrets/gh/hosts.yml -> /run/secrets/gh/hosts.yml). No
configurable root, no sandboxing -- deliberately: this route doesn't cross
a trust boundary the terminal doesn't already cross, so restricting it
would add friction without adding protection.

Always writes 0600, no caller-supplied mode/owner param: there's no other
user to own it as (can't chown without root), and defaulting restrictive
costs nothing.

`save -f` (no `-r`) silently corrupted binary content -- confirmed via
hash mismatch on round-tripped random bytes. Fixed with `-r/--raw`; a real
secret/credential file would have been mangled without this.

Also: --dev only works via cargo run from inside a checkout (the path it
reads is a Rust compile-time constant, baked in wherever the binary was
built) -- doesn't work on an installed/downloaded release binary. Wasn't
documented; found it while debugging why --dev failed on a prebuilt binary.
…sions

umask is process-wide state; scoping it around one request's write in a
server handling concurrent requests would also affect other in-flight
requests' unrelated file creation. chmod-after only narrows the exposure
window to this one destination path.
@cablehead
cablehead merged commit c78df18 into main Jul 2, 2026
4 checks passed
@cablehead
cablehead deleted the feat/filedrop branch July 2, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant