ad4355df17
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
229 lines
11 KiB
Markdown
229 lines
11 KiB
Markdown
# Scratchbox Website Audit Report
|
||
|
||
**URL audited:** https://scratchbox.s1d3sw1ped.com/
|
||
**Date:** 2026-06-01
|
||
**Auditor:** Grok (via Playwright browser automation + direct HTTP/API testing + codebase review)
|
||
**Project commit / state:** Current source in `/fast/projects/golang/scratchbox`
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
Scratchbox is a deliberately minimal, unauthenticated, self-hosted temporary file sharing service. The live deployment matches the project exactly and performs very well for its stated goals.
|
||
|
||
**Strengths**
|
||
- Extremely lightweight (≈ 45 KB total assets).
|
||
- Excellent core functionality and HTTP semantics (201 Created, 410 Gone for expired).
|
||
- Clean error UX and client-side routing.
|
||
- No trackers, no external dependencies, very fast responses.
|
||
- Backend Go code is focused and easy to reason about.
|
||
- Direct API (`POST /api/scratch`) works reliably and returns useful JSON.
|
||
|
||
**Main Issues (prioritized)**
|
||
1. **Security headers completely absent** (CSP, HSTS, X-Content-Type-Options, etc.). High priority for any public or semi-public deployment.
|
||
2. **Poor SEO / social sharing metadata.** Every page (including `/s/{id}` share links) has only a static `<title>Scratchbox</title>`. No Open Graph, no description, no per-scratch titles. Share previews look bad on Slack, Discord, Twitter, etc.
|
||
3. **Missing favicon** (causes repeated 404 noise).
|
||
4. Minor: 404/410 routes return the full SPA shell HTML (acceptable for SPA but not ideal for `robots.txt`, `sitemap.xml`, etc.).
|
||
5. Minor: Console errors logged for expected 410 fetches on expired scratches.
|
||
|
||
The frontend is a Vite + Svelte SPA. The Go backend serves a single static `index.html` shell for all UI routes (`/`, `/u`, `/s/{id}`, 404s, and 410s) via `serveSPAWithStatus`.
|
||
|
||
---
|
||
|
||
## Methodology
|
||
|
||
- Full browser automation with Playwright (navigation, snapshots, screenshots, console, network, clicks, viewport resize to mobile 390×844).
|
||
- Direct HTTP inspection (headers, raw content, timing).
|
||
- Successful end-to-end upload test via both the discovered API (`/api/scratch`) and UI flows.
|
||
- Codebase review of relevant files (`web/static/index.html`, `web/ui/src/App.svelte`, `internal/http/handlers.go`, etc.).
|
||
- One temporary test scratch was created (ID `OvhWwqqXnyA6`, text file, auto-expired after 15m).
|
||
|
||
---
|
||
|
||
## Technical Inventory (Live + Source)
|
||
|
||
| Item | Value / Location |
|
||
|-------------------------|-------------------------------------------------------|
|
||
| HTML shell | `web/static/index.html` (embedded) + `web/ui/index.html` (source) |
|
||
| Frontend source | `web/ui/src/App.svelte`, `web/ui/src/main.js`, `web/ui/vite.config.js` |
|
||
| Built assets | `web/static/assets/index-*.js` (43.5 KB), `*.css` (2.1 KB) |
|
||
| Backend serving | `internal/http/handlers.go` (`serveSPAWithStatus`, `Routes`) |
|
||
| Asset embedding | `web/assets.go` (`//go:embed static`) |
|
||
| API routes | `POST /api/scratch`, `GET /api/scratch/{id}`, `GET /api/raw/{id}`, `GET /s/{id}`, `GET /api/config` |
|
||
| Config exposure | `/api/config` returns `max_upload_size_bytes`, `default_ttl`, `upload_allowed` |
|
||
| No security headers | Confirmed in `handlers.go` and `middleware.go` |
|
||
|
||
---
|
||
|
||
## Detailed Findings
|
||
|
||
### 1. Security Headers (High Priority)
|
||
|
||
**Finding:** No security-related response headers are set for HTML, API, or asset responses.
|
||
|
||
Observed headers (typical):
|
||
```
|
||
server: openresty
|
||
content-type: text/html; charset=utf-8
|
||
x-served-by: scratchbox.s1d3sw1ped.com
|
||
```
|
||
|
||
Missing (recommended minimum):
|
||
- `Content-Security-Policy`
|
||
- `Strict-Transport-Security` (HSTS)
|
||
- `X-Content-Type-Options: nosniff`
|
||
- `Referrer-Policy`
|
||
- `X-Frame-Options` / CSP frame-ancestors
|
||
- `Permissions-Policy`
|
||
|
||
**Impact:** Higher risk of XSS, clickjacking, MIME sniffing, etc., especially since the site accepts and serves user-provided file content (even temporarily).
|
||
|
||
**Files to change:**
|
||
- `internal/http/handlers.go` (in `serveSPAWithStatus`, `getUIConfig`, `rawScratch`, etc.)
|
||
- `internal/http/middleware.go` (good place for a `SecurityHeadersMiddleware`)
|
||
|
||
**Recommendation (minimal):** Add a small middleware that sets safe defaults. Example starting point:
|
||
|
||
```go
|
||
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
h := w.Header()
|
||
h.Set("X-Content-Type-Options", "nosniff")
|
||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||
// Add CSP, HSTS (only over HTTPS), etc. as needed
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
```
|
||
|
||
Register it early in `Routes()`.
|
||
|
||
See also README.md "Public Deployment Notes" — this audit reinforces those recommendations.
|
||
|
||
### 2. SEO & Social Metadata (High Priority for Share Links)
|
||
|
||
**Finding:** The `<head>` in the served shell is static and minimal:
|
||
|
||
```html
|
||
<title>Scratchbox</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<!-- no description, no og:*, no twitter:*, no canonical -->
|
||
```
|
||
|
||
- `/s/{id}` pages always show the generic title.
|
||
- No per-scratch `<title>` or `og:title` containing the filename.
|
||
- No `og:description` or image (even a simple logo would help).
|
||
|
||
**Impact:** When users share `/s/xxx` links, previews are poor or missing.
|
||
|
||
**Files:**
|
||
- `web/static/index.html` (served shell)
|
||
- `web/ui/index.html` (Vite source template)
|
||
- `web/ui/src/App.svelte` — currently no `<svelte:head>` usage for dynamic updates.
|
||
|
||
**Recommendations (in priority order):**
|
||
1. Add basic static meta to both `index.html` files (description + og tags for the generic case).
|
||
2. In `App.svelte`, use `<svelte:head>` inside the `{:else if routeMode === "view"}` branch to set a dynamic `<title>` and `og:*` tags based on `viewMeta.filename`, size, etc.
|
||
3. For best results, consider lightweight server-side injection in `scratchPage` / `serveSPAWithStatus` (parse the ID and inject meta before serving the shell). This gives crawlers and social bots good data even with JS disabled.
|
||
|
||
Example dynamic title idea: `"{filename} • Scratchbox"` (with fallback).
|
||
|
||
### 3. Favicon (Low–Medium)
|
||
|
||
**Finding:** Repeated 404s for `/favicon.ico` in every browser session / page load.
|
||
|
||
**Files:**
|
||
- `web/static/index.html` (add `<link rel="icon" ...>`)
|
||
- `web/ui/index.html` (same)
|
||
- Add a real `favicon.ico` (or SVG) under `web/static/` and ensure it's embedded.
|
||
|
||
### 4. SPA Shell for Non-HTML 404s
|
||
|
||
**Finding:** `robots.txt`, `sitemap.xml`, `.well-known/*`, and invalid asset paths all return the full 394-byte HTML shell with a 404 (or 410) status.
|
||
|
||
This is standard SPA behavior but not ideal for well-known paths.
|
||
|
||
**Current code:** `notFoundPage` → `serveSPAWithStatus(404)` for everything under `GET /{path...}`.
|
||
|
||
**Recommendation:** Add early special cases in the mux for common paths:
|
||
|
||
```go
|
||
mux.Handle("GET /robots.txt", http.HandlerFunc(func(w, r) {
|
||
w.Header().Set("Content-Type", "text/plain")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
// or serve a static disallow file
|
||
}))
|
||
```
|
||
|
||
Or serve small static files for these paths from the embedded FS.
|
||
|
||
### 5. Frontend Form & Upload Details
|
||
|
||
In the current `App.svelte` source (as of audit):
|
||
|
||
- The form uses pure JS (`onsubmit={onSubmit}`) — no `method`/`action` attributes. (The `method="get"` seen in the live DOM during audit was likely a browser default or inspection artifact; source is clean.)
|
||
- File input is a standard `<input type="file" id="file" name="file">` behind a `<label>`.
|
||
- Good validation (size check client-side + server `MaxBytesReader`).
|
||
- Error messages are clear and injected into the DOM.
|
||
|
||
**Minor observation:** No visible drag-and-drop zone (the UI is intentionally minimal). Adding one would be a nice UX improvement but is out of scope for a minimal project unless requested.
|
||
|
||
### 6. Other Observations
|
||
|
||
- **Performance:** Outstanding. 4 requests for a full UI load.
|
||
- **Mobile:** Tested at 390×844 — layout holds up well due to simplicity.
|
||
- **Accessibility:** Basic roles and labels are present (`aria-label="Primary"`, button text, etc.). The file input could benefit from better visual association, but it's functional. A full axe-core or WAVE scan is recommended if public use grows.
|
||
- **Console noise:** The only errors are the favicon 404 and the expected 410 fetch errors when viewing expired scratches (the app correctly catches 404/410 and shows the friendly UI). These could be silenced with `{ok: response.ok}` style guards or by not logging at error level for known cases.
|
||
- **Rate limiting & allowlisting:** Already implemented and configurable (see README). Good.
|
||
- **One test scratch** was created during audit via the public API and viewed successfully. It auto-expired as designed.
|
||
|
||
---
|
||
|
||
## Actionable Recommendations (Minimal Changes)
|
||
|
||
| Priority | Change | Files | Effort |
|
||
|----------|--------|-------|--------|
|
||
| High | Add security headers middleware | `internal/http/middleware.go` + wire in `handlers.go:Routes()` | Low |
|
||
| High | Add basic `<meta>` + OG tags to shell | `web/static/index.html` + `web/ui/index.html` | Low |
|
||
| High | Dynamic title/meta for `/s/{id}` pages | `web/ui/src/App.svelte` (add `<svelte:head>`) | Low–Medium |
|
||
| Medium | Add real favicon + link tag | `web/static/` + the two `index.html` files | Low |
|
||
| Low | Special-case `robots.txt` etc. | `internal/http/handlers.go` | Low |
|
||
| Low | Reduce console.error noise on 410s | `web/ui/src/App.svelte` (in `loadScratchView`) | Low |
|
||
|
||
After any frontend HTML changes, run `make build` (or the equivalent Vite step) to update the embedded assets.
|
||
|
||
---
|
||
|
||
## Strengths Worth Preserving
|
||
|
||
- Intentional minimalism and "no accounts, no friction" philosophy.
|
||
- Transparent storage (gzip + encryption handled server-side).
|
||
- Excellent separation of concerns in Go code.
|
||
- Strong test coverage in `internal/http/` and storage.
|
||
- Clear documentation in `README.md` and `AGENTS.md`.
|
||
|
||
---
|
||
|
||
## Appendix: Raw Data from Audit
|
||
|
||
- API config response: `{"default_ttl":"15m","max_upload_size_bytes":104857600,"upload_allowed":true}`
|
||
- Successful upload JSON (example):
|
||
```json
|
||
{
|
||
"id": "OvhWwqqXnyA6",
|
||
"view_url": "/s/OvhWwqqXnyA6",
|
||
"raw_url": "/api/raw/OvhWwqqXnyA6",
|
||
"expires_at": "2026-06-01T22:31:24.662891754Z",
|
||
...
|
||
}
|
||
```
|
||
- Raw endpoint correctly returns original bytes + `Content-Disposition: attachment; filename=...`
|
||
- All screenshots and full Playwright traces were captured during the session but are not embedded here (available in the auditor's session artifacts if needed).
|
||
|
||
---
|
||
|
||
**End of report.**
|
||
|
||
This audit was performed with full authorization from the site owner. The temporary test scratch created during testing has long since expired.
|
||
|
||
If you would like me to implement any of the recommendations (as minimal, focused patches per `AGENTS.md`), provide the exact priorities and I can prepare the diffs. |