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