Human
Machine
REPRO-2026-00158 MEDIUM CSRF
Verified
goshs: PUT upload accepts cross-origin requests without CSRF token
github.com/patrickhener/goshs (go) May 23, 2026
What's the vulnerability?
The PUT upload handler in httpserver/updown.go lacks the CSRF token that
the POST upload handler requires. Combined with permissive CORS
(Access-Control-Allow-Origin: * on the OPTIONS preflight), any cross-origin
page the victim visits can issue an HTTP PUT and write arbitrary files
to a goshs server reachable from the victim's browser (loopback, LAN, etc.).
The fix adds CSRF-token validation to the PUT path so unauthenticated cross-origin uploads are rejected.
Root Cause Analysis
# RCA Report: CVE-2026-42091 — goshs PUT Upload CSRF
## Summary
goshs (a Go-based HTTP file server CLI) versions ≤ v2.0.1 are vulnerable to Cross-Site Request Forgery (CSRF) on the PUT upload endpoint. While the POST upload handler enforced CSRF token validation via `checkCSRF()`, the `put()` handler in `httpserver/updown.go` omitted this check entirely. Combined with permissive CORS headers (`Access-Control-Allow-Origin: *`), a malicious cross-origin webpage can silently issue an HTTP PUT and write arbitrary files to any reachable goshs instance.
## Impact
- **Package**: `github.com/patrickhener/goshs`
- **Affected versions**: `≤ v2.0.1`
- **Fixed version**: `v2.0.2`
- **CWE**: CWE-352 (Cross-Site Request Forgery)
- **Severity**: Medium (CVSS 3.1 base 6.5)
- **Risk**: Any website visited by a user whose browser can reach a goshs upload server (loopback, LAN, VPN, etc.) can upload arbitrary files without user consent.
## Root Cause
The `put()` handler in `httpserver/updown.go` did not call `fs.checkCSRF(w, req)` before processing the request. The `checkCSRF()` function (present in `httpserver/handler.go`) validates the `X-CSRF-Token` header, absence of `Origin`/`Referer` (non-browser clients), or same-origin requests. Because `put()` skipped this validation, a browser-initiated cross-origin PUT request—bearing an arbitrary `Origin` header but no valid token—was accepted and the file was written to disk.
The fix commit [`0e715b94e10c3d1aa552276000f15f104dee2f32`](https://github.com/patrickhener/goshs/commit/0e715b94e10c3d1aa552276000f15f104dee2f32) adds the missing `checkCSRF()` call at the top of `put()`.
## Reproduction Steps
The reproduction is fully automated by `repro/reproduction_steps.sh`. It performs the following:
1. Clones the `goshs` repository (or reuses an existing clone).
2. Builds the vulnerable binary at tag `v2.0.1` (`goshs_v2.0.1`).
3. Builds the fixed binary at tag `v2.0.2` (`goshs_v2.0.2`).
4. Starts each binary in a separate scratch upload directory with upload enabled.
5. Sends a cross-origin-style PUT request (`curl -X PUT -H "Origin: http://evil.com" --data-binary 'pwned' http://localhost:<port>/pwned.txt`).
6. Captures the HTTP response status and checks whether `pwned.txt` was created.
**Expected evidence of reproduction:**
- **v2.0.1 (vulnerable)**: HTTP `200 OK` and `pwned.txt` is created in the upload directory.
- **v2.0.2 (fixed)**: HTTP `403 Forbidden` and `pwned.txt` is **not** created.
## Evidence
All logs are written to `logs/`:
- `logs/vuln.log` — goshs v2.0.1 server output.
- `logs/vuln_result.json` — captured HTTP status (`200`), response body, and `file_created: true`.
- `logs/fixed.log` — goshs v2.0.2 server output.
- `logs/fixed_result.json` — captured HTTP status (`403`), response body (`Forbidden`), and `file_created: false`.
Excerpt from `logs/vuln_result.json`:
```json
{
"version": "vuln",
"http_status": "200",
"response_body": "",
"file_created": true,
"upload_dir": ".../logs/upload_vuln"
}
```
Excerpt from `logs/fixed_result.json`:
```json
{
"version": "fixed",
"http_status": "403",
"response_body": "Forbidden",
"file_created": false,
"upload_dir": ".../logs/upload_fixed"
}
```
Environment:
- Go version used for build: `go1.24.7` (v2.0.1) / `go1.25.0` (v2.0.2)
- OS: Linux (containerized)
- Network: localhost (`127.0.0.1`)
## Recommendations / Next Steps
1. **Immediate fix**: Upgrade to `goshs v2.0.2` or later, which enforces `checkCSRF()` on every mutating handler (PUT, POST, DELETE).
2. **Defense in depth**: If running goshs on a publicly reachable interface, pair it with `--basic-auth` (`-b user:pass`). When basic auth is configured, the CORS preflight already fails in browsers, providing an additional layer of protection.
3. **Regression testing**: Any future change to upload handlers should be accompanied by a test that sends a cross-origin request with a mismatched `Origin` header and asserts `403 Forbidden`.
## Additional Notes
- **Idempotency**: The reproduction script was executed twice consecutively and produced identical results both times.
- **Edge cases**: The script simulates a browser cross-origin request by explicitly sending an `Origin` header. Without this header, `curl` requests are treated as non-browser clients and are allowed by design (see `checkCSRF` logic). The vulnerability specifically affects browser-based CSRF attacks.
- **Limitations**: The reproduction does not require a real browser or a second origin; the malicious cross-origin behavior is accurately modeled with `curl` and the `Origin` header.
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00158 or
pruva-verify GHSA-rhf7-wvw3-vjvm or
pruva-verify CVE-2026-42091 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00158/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available