# 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 `
Scratchbox`. 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 `` in the served shell is static and minimal:
```html
Scratchbox
```
- `/s/{id}` pages always show the generic title.
- No per-scratch `` 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 `` 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 `` inside the `{:else if routeMode === "view"}` branch to set a dynamic `` 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 ``)
- `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 `` behind a `