Remove cans feature; add Secure flag to pw_/tok_ cookies
Owner decision 2026-10-16: the cans feature (bundles of pastes, #4) was half-integrated and has zero prod usage; removing it eliminates four audit findings by deletion. Removed: - internal/api/cans.go and all can routes (/api/pastes/can, /api/cans/*, /can/{id}) plus handleCanPage/renderCan - is_can from /api/mine and /api/public responses - store layer: CanRow, can store funcs, paste_cans UNION in listings, cans sweep; reserved slugs 'can'/'cans' released - templates/can.html, can CSS, is_can badges in history.js/mine.js - cans tests (cans_test.go, cans_flow_test.go, cans_expiry_test.go, TestErrorCodeCanValidation) - docs/api.md cans section, README/LICENSE/docker-compose mentions Store migration: drops paste_cans only when empty (non-empty table is kept and logged; prod verified clean 2026-10-16) and drops the dormant pastes.can_id column. Covered by new TestMigrateDropsCansSchema. Security hardening (audit findings): pw_<id> (unlock) and tok_<id> (deletion-token) cookies now carry Secure: true in addition to HttpOnly + SameSite=Lax; all deployments are HTTPS-only. cookie_set_test now asserts Secure. go vet ./... and go test ./... pass.
This commit is contained in:
1 parent
651bc0cb86
commit
e54fbc36ef
20 files changed
+176
-1356
No files matched your search
+5
-28
@@ -1,5 +1,5 @@
|
||||
// Package web serves palette's HTML routes: paste pages, cans, unlock, and
|
||||
// the admin page. Templates and static assets are embedded in this package.
|
||||
// Package web serves palette's HTML routes: paste pages, unlock, and the
|
||||
// admin page. Templates and static assets are embedded in this package.
|
||||
package web
|
||||
|
||||
import (
|
||||
@@ -111,29 +111,6 @@ func (h *Handlers) RenderNotFoundPage(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderNotFound(w, r, r.URL.Path)
|
||||
}
|
||||
|
||||
// RenderPage is the exported wrapper used by the api package (#4 can pages).
|
||||
func (h *Handlers) RenderPage(w http.ResponseWriter, name string, data any) {
|
||||
h.renderPage(w, name, data)
|
||||
}
|
||||
|
||||
// WriteRateLimited is the exported rate-limit response used by the api package (#4).
|
||||
func (h *Handlers) WriteRateLimited(w http.ResponseWriter, retryAfterSecs int) {
|
||||
h.writeRateLimited(w, retryAfterSecs)
|
||||
}
|
||||
|
||||
// UnlockToken returns the per-id HMAC unlock token (cookie value, #34).
|
||||
// Exported for the api package so can pages share paste cookie semantics (#4).
|
||||
func UnlockToken(id string) string { return unlockToken(id) }
|
||||
|
||||
// AgoString formats a relative "N ago" string (exported for the api package, #4).
|
||||
func AgoString(ts int64) string { return agoString(ts) }
|
||||
|
||||
// HumanSize formats a byte count as a human string (exported for the api package, #4).
|
||||
func HumanSize(n int) string { return humanSize(n) }
|
||||
|
||||
// ExpiryString formats remaining time until an epoch seconds expiry (#4).
|
||||
func ExpiryString(expiresAt int64) string { return expiryString(expiresAt) }
|
||||
|
||||
// #34: per-paste unlock tokens. unlockSecret is generated once at startup
|
||||
// (also derivable from PALETTE_UNLOCK_SECRET for multi-instance deploys) and
|
||||
// used to HMAC paste ids, so a client can only hold a valid pw_<id> cookie by
|
||||
@@ -369,7 +346,7 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) {
|
||||
// the paste id under the server's random secret.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pw_" + row.ID, Value: unlockToken(row.ID), Path: "/",
|
||||
MaxAge: 3600, HttpOnly: true, SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: 3600, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: true,
|
||||
})
|
||||
// re-render without lock, or redirect if ?next= was given (#26)
|
||||
if next := r.FormValue("next"); next != "" {
|
||||
@@ -403,7 +380,7 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if justCreated && token != "" {
|
||||
// one-time display of the deletion token via sessionStorage (#143)
|
||||
http.SetCookie(w, &http.Cookie{Name: "tok_" + row.ID, Value: token, Path: "/", MaxAge: 60, HttpOnly: true, SameSite: http.SameSiteLaxMode})
|
||||
http.SetCookie(w, &http.Cookie{Name: "tok_" + row.ID, Value: token, Path: "/", MaxAge: 60, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: true})
|
||||
}
|
||||
// Count the view for real page renders, deduped per-viewer within the
|
||||
// burn window (#95): a reload by the same vwr cookie doesn't inflate
|
||||
@@ -466,7 +443,7 @@ func (h *Handlers) HandleAdminPage(w http.ResponseWriter, r *http.Request) {
|
||||
func (u *UI) Handlers() *Handlers { return &Handlers{UI: u} }
|
||||
|
||||
// #59: security headers for rendered HTML pages. Applied wherever the
|
||||
// response is text/html (page templates and the inline can page); JSON API
|
||||
// response is text/html (page templates); JSON API
|
||||
// responses and /raw content pass through untouched.
|
||||
// #139: script-src and style-src no longer allow 'unsafe-inline'. All
|
||||
// previously-inline scripts moved to external files under static/ (page data
|
||||
|
||||
Reference in new issue
Block a user