Skip to content

fix(security): harden apish - path traversal, CORS, auth, and cleanup - #2

Open
thbkrkr wants to merge 36 commits into
masterfrom
review-fixes
Open

fix(security): harden apish - path traversal, CORS, auth, and cleanup#2
thbkrkr wants to merge 36 commits into
masterfrom
review-fixes

Conversation

@thbkrkr

@thbkrkr thbkrkr commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Security: prevent path traversal in script execution; fix wildcard+credentials CORS combo; compare API key in constant time; gate /docker behind a flag then remove it entirely; configurable basic-auth user with no default API key; warn when running unauthenticated
  • Fixes: handle errors in POST handler; return 204 for favicon; treat any stat error as missing index; log and exit on server error; derive /ls URL scheme from the request
  • Refactor: expose /version without auth; deduplicate GET/POST exec handlers; unify logging on logrus; single static walk with real error propagation; drop favicon route and slim CORS headers
  • Build: multi-stage Dockerfile with Go 1.25; drop dead release target
  • Docs: KISS README rewrite with inline flag comments, real curl output, layout tree; example scripts build JSON safely with jq
  • Tests: add coverage for path traversal, POST, invalid JSON, /ls, script failure, wrong credentials; fix broken apiDir path after restructuring; simplify HTTP helpers
  • Helm: minimal chart (Deployment + Service) with configurable image, port, auth flags; helm-* Makefile targets using apish as release name
  • Build: bump Go 1.25 → 1.26.4 and alpine 3.20 → 3.23; tag example image as krkr/apish:example; add push and port-forward targets to example Makefile

Test plan

  • go test ./... passes
  • helm template renders without errors (make helm-render)
  • ./go-apish -apiDir=example serves scripts and static files
  • Auth is required when -password is set; server warns when it is not
  • make -C example build && make -C example run serves the example image on port 80

thbkrkr added 30 commits June 2, 2026 17:38
Validate the wildcard request path resolves within ApiDir before
executing, rejecting escapes like /api/../../../tmp/evil that could
run arbitrary .sh files anywhere on the filesystem.
The /docker endpoint runs arbitrary 'docker run' commands, which is
effectively root on the host. Make it opt-in (off by default) and
replace strings.Split with a quote-aware tokenizer so quoted args with
spaces are preserved.
Access-Control-Allow-Origin: * together with Allow-Credentials: true is
rejected by browsers. Remove the credentials header and add X-Auth to
the allowed request headers so header auth works cross-origin.
Use crypto/subtle.ConstantTimeCompare for the X-Auth header check to
avoid timing side channels.
When -password is unset the server is fully open; emit a loud startup
warning so this isn't silently the default.
The canonical examples interpolated values straight into the JSON
output; $1 in param.sh could break the JSON (or worse in a script that
evaluates it). Use 'jq -n --arg/--argjson' so values are always
correctly typed and escaped.
PostExecScript discarded the Start()/Wait() errors with '_ =' and then
checked a never-assigned err, so failing scripts fell through to a
confusing 'Invalid JSON' 400. Use cmd.Run(), check its error, and
capture stderr for the error log.
c.JSON(200, nil) sent the literal 'null' as a favicon. Return 204
instead, and update the test expectation.
Previously a non-NotExist error (e.g. permissions) returned true and
triggered a redirect to a file that can't be read. Only treat a nil
error as present.
The bare 'for { s.ListenAndServe() }' silently restarted the server on
error (busy-looping when the port is unavailable) and never logged why.
Log the error and exit. Also clarify the startup log measures setup time.
Move /version out of the auth group so health/version checks work even
when -password is set.
Extract shared scriptPath validation, existence check (resolve) and
JSON-emitting execution (run). ExecScript and PostExecScript now differ
only in how the command is built (query arg vs stdin body). GET also
gains stderr capture for consistency.
Replace the standard 'log' package and fmt.Printf logging in main.go,
router.go and handlers with logrus (already used by docker.go), and drop
the redundant [info]/[error]/[warn] prefixes now that levels convey them.
Store ApiDir as string instead of *string in ExecHandler/LsHandler. The
flag value is resolved once at Router() construction, removing handler
dependence on a mutable global pointer.
Collapse the two _static walks into one pass that splits HTML pages from
other files, and propagate walk errors (the walk funcs previously always
returned nil, making the error checks dead code). The optional _static
directory is skipped when absent.
fileToUrl hardcoded http://, producing wrong links behind TLS/a proxy.
Build the base URL from the request scheme (TLS or X-Forwarded-Proto)
and pass it in. Also switch strings.Replace(-1) to strings.ReplaceAll.
Add a -user flag for the basic-auth username (was a hardcoded global)
and default -apiKey to empty. An empty key now disables X-Auth header
auth entirely so it can't accidentally match a missing header.
Replace the golang:1.6.2 + alpine:3.7 build (which can't build a go 1.25
module) with a multi-stage Dockerfile on golang:1.25-alpine + alpine:3.20.
The image now builds itself; 'make binary' builds locally and 'make build'
builds the image, both injecting git commit/date via ldflags.
The 'make release' target called ./release.sh, which is not in the repo.
Remove it; image publishing is covered by 'make push'.
Add a flags table, endpoint reference, the JSON output contract, the
authentication model, and a security section covering -password and the
-enableDocker host-access risk.
Add a handler unit test asserting scriptPath rejects escapes from
ApiDir, plus HTTP tests for POST execution, invalid-JSON 400, /ls
listing, and a path-traversal request. Fix TestBase for the /version
no-auth route and the authenticated / -> /s redirect.
Key the test.BasicAuth literals (go vet warned on unkeyed fields) and
remove the unused 'reader' variable and its 'io' import.
The docker run endpoint is no longer used. Remove the handler, the
-enableDocker flag, its route, the example docker script/page, and the
related README docs.
GOMAXPROCS already defaults to NumCPU since Go 1.5, so the manual call
was a no-op. Drop the function and the runtime import.
Rename Get2 to GetWithKey with a plain string key, replace deprecated
ioutil.ReadAll with io.ReadAll, make the unexported do() fail fast with
t.Fatal, and close the response body.
Remove the /favicon.ico 204 convenience route (let it 404 like any
other unknown path) and trim the CORS middleware to the origin, methods
and headers this API actually uses.
Drop the most complex example script; the simpler date/param/post
examples are enough to show the JSON contract.
Inline the ~60-line HTTP helper package as unexported helpers in the
single test file, dropping the separate package and the ServerURL/
PrefixURL globals (helpers use the test server directly).
cdn.ractivejs.org is defunct and the http:// URL was blocked as mixed
content on HTTPS pages. Point to a pinned jsDelivr build over HTTPS.
thbkrkr and others added 6 commits June 2, 2026 23:26
Move exec.go, ls.go, cors.go, auth.go (and exec_test.go) to the root as
package main and drop the cross-package imports. For a project this small
a single package is simpler than three.
- Go source files moved to app/ so the root stays clean
- example/api/{test,time,_static} flattened to example/{test,time,_static}
- Added fail.sh with proper shebang for clean 500 error output
- Updated README: KISS rewrite with inline flag comments, curl examples with real output, and layout tree showing dir→URL mapping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use filepath.Abs for ApiDir in ListResources (avoids ReplaceAll mangling
the leading ../), point the tests and README run example at ../example /
example now that the api/ level is gone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile: Go 1.25 → 1.26.4, alpine 3.20 → 3.23
- example/Dockerfile: tag as krkr/apish:example, add entrypoint CMD
- example/Makefile: push target, fix run image name, add port-forward
- helm/values.yaml: default image tag to example, pullPolicy Always

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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