11 KiB
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)
- Security headers completely absent (CSP, HSTS, X-Content-Type-Options, etc.). High priority for any public or semi-public deployment.
- 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. - Missing favicon (causes repeated 404 noise).
- Minor: 404/410 routes return the full SPA shell HTML (acceptable for SPA but not ideal for
robots.txt,sitemap.xml, etc.). - 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-PolicyStrict-Transport-Security(HSTS)X-Content-Type-Options: nosniffReferrer-PolicyX-Frame-Options/ CSP frame-ancestorsPermissions-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(inserveSPAWithStatus,getUIConfig,rawScratch, etc.)internal/http/middleware.go(good place for aSecurityHeadersMiddleware)
Recommendation (minimal): Add a small middleware that sets safe defaults. Example starting point:
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:
<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>orog:titlecontaining the filename. - No
og:descriptionor 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):
- Add basic static meta to both
index.htmlfiles (description + og tags for the generic case). - In
App.svelte, use<svelte:head>inside the{:else if routeMode === "view"}branch to set a dynamic<title>andog:*tags based onviewMeta.filename, size, etc. - 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) underweb/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:
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}) — nomethod/actionattributes. (Themethod="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.mdandAGENTS.md.
Appendix: Raw Data from Audit
- API config response:
{"default_ttl":"15m","max_upload_size_bytes":104857600,"upload_allowed":true} - Successful upload JSON (example):
{ "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.