fix(security): harden apish - path traversal, CORS, auth, and cleanup - #2
Open
thbkrkr wants to merge 36 commits into
Open
fix(security): harden apish - path traversal, CORS, auth, and cleanup#2thbkrkr wants to merge 36 commits into
thbkrkr wants to merge 36 commits into
Conversation
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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/dockerbehind a flag then remove it entirely; configurable basic-auth user with no default API key; warn when running unauthenticated/lsURL scheme from the request/versionwithout auth; deduplicate GET/POST exec handlers; unify logging on logrus; single static walk with real error propagation; drop favicon route and slim CORS headersreleasetargetjq/ls, script failure, wrong credentials; fix brokenapiDirpath after restructuring; simplify HTTP helpershelm-*Makefile targets usingapishas release namekrkr/apish:example; addpushandport-forwardtargets to example MakefileTest plan
go test ./...passeshelm templaterenders without errors (make helm-render)./go-apish -apiDir=exampleserves scripts and static files-passwordis set; server warns when it is notmake -C example build && make -C example runserves the example image on port 80