diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2026-10-20 17:33:56 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2026-10-20 18:02:54 -0700 |
| commit | 9b0e3f5b92f20d7743b09a8e285a39d32c72b926 (patch) | |
| tree | e7fb08b000e8c48962434e76c930e30ef12aa675 | |
| parent | Remove a lot of files that aren't necessary (diff) | |
| download | roseh.moe-9b0e3f5b92f20d7743b09a8e285a39d32c72b926.tar.zst | |
Fix some random security issues
| -rw-r--r-- | roseh.moe.go | 164 | ||||
| -rw-r--r-- | templates/register/register.html.template | 4 |
2 files changed, 106 insertions, 62 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index f497c8e..67b13c1 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -36,6 +36,7 @@ import ( "embed" "encoding/base64" "encoding/binary" + "encoding/json" "encoding/xml" "errors" "flag" @@ -188,7 +189,7 @@ type serviceConfiguration struct { PackageRedirects []packageConfig `xml:"Redirects>Package"` CommandRedirects []commandConfig `xml:"Redirects>Command"` Passkey []passkey - passkeys map[int64][]byte + passkeys map[string][]byte } var ( @@ -220,8 +221,8 @@ func loadConfig() (*serviceConfiguration, error) { return nil, err } names := make(map[string]bool) - config.passkeys = make(map[int64][]byte) - namesByID := make(map[int64]string) + config.passkeys = make(map[string][]byte) + namesByID := make(map[string]string) for _, passkey := range config.Passkey { if names[passkey.ID] { return nil, fmt.Errorf("%s: duplicate name %q", *configFile, passkey.ID) @@ -229,21 +230,20 @@ func loadConfig() (*serviceConfiguration, error) { names[passkey.ID] = true keyBytes, err := base64.RawURLEncoding.DecodeString(passkey.Key) if err != nil { - return nil, fmt.Errorf("%s: invalid key for device %q: %s", *configFile, passkey.ID, err) + return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err) } - id, n := binary.Varint(keyBytes) - if n <= 0 { - return nil, fmt.Errorf("%s: invalid key for device %q: bad device ID", *configFile, passkey.ID) + if len(keyBytes) < 16 { + return nil, fmt.Errorf("%s: invalid key for passkey %q: bad user ID: too short", *configFile, passkey.ID) } - key := keyBytes[n:] + id, key := keyBytes[:16], keyBytes[16:] if _, err := sigVerifier(key); err != nil { - return nil, fmt.Errorf("%s: invalid key for device %q: %s", *configFile, passkey.ID, err) + return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err) } - if _, ok := config.passkeys[id]; ok { - return nil, fmt.Errorf("%s: passkeys %q and %q have duplicate ID %d", *configFile, namesByID[id], passkey.ID, id) + if _, ok := config.passkeys[string(id)]; ok { + return nil, fmt.Errorf("%s: passkeys %q and %q have duplicate ID %d", *configFile, namesByID[string(id)], passkey.ID, id) } - config.passkeys[id] = key - namesByID[id] = passkey.ID + config.passkeys[string(id)] = key + namesByID[string(id)] = passkey.ID } slices.SortFunc(config.PackageRedirects, func(x, y packageConfig) int { return strings.Compare(x.Name, y.Name) }) slices.SortFunc(config.CommandRedirects, func(x, y commandConfig) int { return strings.Compare(x.Name, y.Name) }) @@ -774,7 +774,7 @@ func verifyHMAC(signedMsg, purpose string) (string, bool) { const ( authVersion = "authv3" - authCookieName = "a" + authCookieName = "__Secure-a" ) func makeHTTPCookie(token string, expiry time.Time) *http.Cookie { @@ -791,9 +791,16 @@ func makeHTTPCookie(token string, expiry time.Time) *http.Cookie { } } -func attachCookie(w http.ResponseWriter, expiry time.Time) { - token := signMsg(base64.RawURLEncoding.EncodeToString(binary.AppendVarint(nil, expiry.Unix())), authVersion) - http.SetCookie(w, makeHTTPCookie(token, expiry)) +type authToken struct { + expiry time.Time + userID []byte +} + +func attachCookie(w http.ResponseWriter, tok *authToken) { + buf := binary.AppendVarint(nil, tok.expiry.Unix()) + buf = append(buf, tok.userID...) + token := signMsg(base64.RawURLEncoding.EncodeToString(buf), authVersion) + http.SetCookie(w, makeHTTPCookie(token, tok.expiry)) } func cookieAuth(r *http.Request) bool { @@ -813,10 +820,25 @@ func cookieAuth(r *http.Request) bool { if n <= 0 { return false } + msgBytes = msgBytes[n:] expiry := time.Unix(expiryUnix, 0) if time.Now().After(expiry) { return false } + if len(msgBytes) > 0 { + if len(msgBytes) < 16 { + return false + } + userID := msgBytes + config, err := loadConfig() + if err != nil { + log.Printf("Error: failed to load config: %s", err) + return false + } + if _, ok := config.passkeys[string(userID)]; !ok { + return false + } + } return true } @@ -872,16 +894,11 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "Challenge is too old. Please try again", http.StatusConflict) return } - userBytes, err := base64.StdEncoding.DecodeString(r.FormValue("user")) + user, err := base64.StdEncoding.DecodeString(r.FormValue("user")) if err != nil { http.Error(w, "Bad user", http.StatusBadRequest) return } - user, n := binary.Varint(userBytes) - if n <= 0 { - http.Error(w, "Bad user", http.StatusBadRequest) - return - } authData, err := base64.StdEncoding.DecodeString(r.FormValue("auth-data")) if err != nil { http.Error(w, "Bad auth data", http.StatusBadRequest) @@ -892,9 +909,25 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "Bad client data: invalid base64", http.StatusBadRequest) return } + var cd struct { + Type string `json:"type"` + Challenge string `json:"challenge"` + Origin string `json:"origin"` + CrossOrigin bool `json:"crossOrigin"` + } + if err := json.Unmarshal(clientData, &cd); err != nil { + http.Error(w, "Bad client data: invalid JSON", http.StatusBadRequest) + return + } challengeBase64 := base64.RawURLEncoding.EncodeToString(myChallenge) - if !strings.Contains(string(clientData), challengeBase64) { - http.Error(w, "Bad client data: bad challenge", http.StatusBadRequest) + if cd.Type != "webauthn.get" || cd.Challenge != challengeBase64 || cd.Origin != selfURL.String() || cd.CrossOrigin { + http.Error(w, "Bad client data", http.StatusBadRequest) + return + } + expectedRPHash := sha256.Sum256([]byte(selfURL.Hostname())) + const flagUserPresence, flagUserVerification = 0x01, 0x04 + if len(authData) < 37 || !bytes.Equal(authData[:32], expectedRPHash[:]) || authData[32]&(flagUserPresence|flagUserVerification) != (flagUserPresence|flagUserVerification) { + http.Error(w, "Bad authenticator data", http.StatusBadRequest) return } sig, err := base64.StdEncoding.DecodeString(r.FormValue("sig")) @@ -908,7 +941,7 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal error", http.StatusInternalServerError) return } - pubKey, ok := config.passkeys[user] + pubKey, ok := config.passkeys[string(user)] if !ok { http.Error(w, "No user with the given ID", http.StatusBadRequest) return @@ -918,7 +951,10 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "Challenge failed", http.StatusBadRequest) return } - attachCookie(w, time.Now().Add(30*24*time.Hour)) + attachCookie(w, &authToken{ + expiry: time.Now().Add(30 * 24 * time.Hour), + userID: user, + }) redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect") if !ok { redirect = "/" @@ -938,22 +974,12 @@ type registerTemplateArgs struct { } func serveRegister(w http.ResponseWriter, r *http.Request) { - serviceConfig, err := loadConfig() - if err != nil { - log.Printf("Error: failed to load service config: %s", err) - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - var maxID int64 - for id := range serviceConfig.passkeys { - maxID = max(maxID, id) - } - myID := maxID + 1 - buf := make([]byte, 32) + buf := make([]byte, 32+16) rand.Read(buf) + challenge, userID := buf[:32], buf[32:] if err := registerTemplate.Execute(w, registerTemplateArgs{ - ID: base64.StdEncoding.EncodeToString(binary.AppendVarint(nil, myID)), - Challenge: base64.StdEncoding.EncodeToString(buf), + ID: base64.StdEncoding.EncodeToString(userID), + Challenge: base64.StdEncoding.EncodeToString(challenge), }); err != nil { log.Printf("Warning: serveRegister: %s", err) } @@ -976,18 +1002,6 @@ func register(w http.ResponseWriter, r *http.Request) { http.Error(w, "Passkey name is required", http.StatusBadRequest) return } - config, err := loadConfig() - if err != nil { - log.Printf("Error: failed to load config: %s", err) - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - for _, passkey := range config.Passkey { - if passkey.ID == username { - http.Error(w, "Passkey name already used", http.StatusBadRequest) - return - } - } userID, err := base64.StdEncoding.DecodeString(r.FormValue("id")) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) @@ -1066,7 +1080,7 @@ func otpLogin(w http.ResponseWriter, r *http.Request) { http.Error(w, "OTP is too old. Please generate a new link", http.StatusConflict) return } - attachCookie(w, time.Now().Add(24*time.Hour)) + attachCookie(w, &authToken{expiry: time.Now().Add(24 * time.Hour)}) fmt.Fprintf(w, "Temporary 24 hour token issued. You are now logged in.\n") } @@ -1330,7 +1344,15 @@ func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "not logged in", http.StatusUnauthorized) } else { u := *r.URL - u.Host = r.Host + switch r.Host { + case + "cinema." + selfURL.Host, + "downloads." + selfURL.Host: + + u.Host = r.Host + default: + u.Host = selfURL.Host + } u.Scheme = "http" if *https != "" { u.Scheme = "https" @@ -1376,6 +1398,24 @@ func (h *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.h.ServeHTTP(w, r) } +type removeAuthCookieHandler struct { + h http.Handler +} + +func (h *removeAuthCookieHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + /*var cookies []*http.Cookie + for _, c := range r.Cookies() { + if c.Name != authCookieName { + cookies = append(cookies, c) + } + } + r.Header.Del("Cookie") + for _, c := range cookies { + r.AddCookie(c) + }*/ + h.h.ServeHTTP(w, r) +} + func main() { flag.Parse() @@ -1457,17 +1497,17 @@ func main() { mux.Handle("/api/", apiMux) mux.Handle("/", &timeoutHandler{h: http.HandlerFunc(sharedFileHandler), timeout: 5 * time.Minute}) serverMux := http.NewServeMux() - serverMux.Handle(selfURL.Hostname()+"/", http.NewCrossOriginProtection().Handler(&gzipHandler{mux})) + serverMux.Handle(selfURL.Hostname()+"/", &gzipHandler{mux}) jellyfinURL, err := url.Parse("http://127.0.0.1:8096") if err != nil { log.Fatal(err) } - serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &httputil.ReverseProxy{ + serverMux.Handle("cinema."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(jellyfinURL) r.SetXForwarded() }, - }}) + }}}) codeMux := http.NewServeMux() cgitStaticFiles := http.FileServer(http.Dir(*cgit)) codeMux.Handle("/cgit.css", &timeoutHandler{h: cgitStaticFiles, timeout: 5 * time.Minute}) @@ -1483,20 +1523,20 @@ func main() { Env: []string{"CGIT_CONFIG=" + cgitConfigAbsPath}, } cgitCGI.PathLocationHandler = cgitCGI - codeMux.Handle("/", &timeoutHandler{h: cgitCGI, timeout: 5 * time.Minute}) + codeMux.Handle("/", &timeoutHandler{h: &removeAuthCookieHandler{cgitCGI}, timeout: 5 * time.Minute}) serverMux.Handle("code."+selfURL.Hostname()+"/", &gzipHandler{codeMux}) transmissionURL, err := url.Parse("http://127.0.0.1:9091") if err != nil { log.Fatal(err) } - serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &httputil.ReverseProxy{ + serverMux.Handle("downloads."+selfURL.Hostname()+"/", &authHandler{h: &removeAuthCookieHandler{&httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(transmissionURL) r.SetXForwarded() }, - }}) + }}}) server := &http.Server{ - Handler: &loggingHandler{serverMux}, + Handler: &loggingHandler{http.NewCrossOriginProtection().Handler(serverMux)}, ReadHeaderTimeout: 5 * time.Minute, IdleTimeout: 10 * time.Minute, diff --git a/templates/register/register.html.template b/templates/register/register.html.template index 3b4d959..833a1a7 100644 --- a/templates/register/register.html.template +++ b/templates/register/register.html.template @@ -40,6 +40,10 @@ async function generateKey() { const name = document.getElementById("name").value; const publicKeyCredential = await navigator.credentials.create({publicKey: { + authenticatorSelection: { + residentKey: "required", + userVerification: "required", + }, challenge: Uint8Array.fromBase64("{{.Challenge}}").buffer, pubKeyCredParams: [ {alg: -9, type: "public-key"}, |
