pentest: bind unlock cookie to HMAC per-paste token; serve only safe content types on /raw and can items with nosniff (#34)
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -58,6 +64,29 @@ func renderPage(w http.ResponseWriter, name string, data any) {
|
||||
|
||||
var webUIInstance *webUI
|
||||
|
||||
// #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
|
||||
// actually submitting the correct password for that paste.
|
||||
var unlockSecret = resolveUnlockSecret()
|
||||
|
||||
func resolveUnlockSecret() []byte {
|
||||
if v := os.Getenv("PALETTE_UNLOCK_SECRET"); v != "" {
|
||||
return []byte(v)
|
||||
}
|
||||
b := make([]byte, 32)
|
||||
if _, err := cryptorand.Read(b); err != nil {
|
||||
log.Fatal("cannot generate unlock secret: ", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func unlockToken(id string) string {
|
||||
mac := hmac.New(sha256.New, unlockSecret)
|
||||
mac.Write([]byte("unlock:" + id))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *apiServer) handleNewPage(w http.ResponseWriter, r *http.Request) {
|
||||
renderPage(w, "new.html", map[string]any{"Page": "new"})
|
||||
}
|
||||
@@ -172,8 +201,12 @@ func (a *apiServer) handlePasteView(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
pw := r.FormValue("password")
|
||||
if pw != "" && checkPassword(row.PasswordHash.String, pw) {
|
||||
// #34: the unlock cookie must be bound to this specific paste and
|
||||
// unforgable. A static value ("1") let anyone bypass the password
|
||||
// by setting pw_<id>=1 for any paste id. The token is an HMAC of
|
||||
// the paste id under the server's random secret.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pw_" + row.ID, Value: "1", Path: "/",
|
||||
Name: "pw_" + row.ID, Value: unlockToken(row.ID), Path: "/",
|
||||
MaxAge: 3600, HttpOnly: true, SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
// re-render without lock, or redirect if ?next= was given (#26)
|
||||
@@ -190,9 +223,9 @@ func (a *apiServer) handlePasteView(w http.ResponseWriter, r *http.Request) {
|
||||
renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": true, "CreatedAgo": agoString(row.CreatedAt), "CreatedAtUnix": row.CreatedAt})
|
||||
return
|
||||
}
|
||||
// check cookie
|
||||
// check cookie — must carry the valid per-paste unlock token (#34)
|
||||
c, err := r.Cookie("pw_" + row.ID)
|
||||
if err != nil || c.Value != "1" {
|
||||
if err != nil || c.Value != unlockToken(row.ID) {
|
||||
renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": false, "CreatedAgo": agoString(row.CreatedAt), "CreatedAtUnix": row.CreatedAt})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user