From eeb02858d9068b0f942912531f0bac59b5925441 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 17 Oct 2026 11:37:40 +0000 Subject: [PATCH] types: accept only lowercase hex in auth IDs --- hscontrol/handlers.go | 3 --- hscontrol/types/common.go | 8 ++++++ hscontrol/types/common_test.go | 45 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/hscontrol/handlers.go b/hscontrol/handlers.go index e34c67d5c..8a5bc2747 100644 --- a/hscontrol/handlers.go +++ b/hscontrol/handlers.go @@ -377,9 +377,6 @@ func authIDFromRequest(req *http.Request) (types.AuthID, error) { return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err)) } - // We need to make sure we dont open for XSS style injections, if the parameter that - // is passed as a key is not parsable/validated as a NodePublic key, then fail to render - // the template and log an error. authId, err := types.AuthIDFromString(raw) if err != nil { return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err)) diff --git a/hscontrol/types/common.go b/hscontrol/types/common.go index a5a6eedc5..9b11b068a 100644 --- a/hscontrol/types/common.go +++ b/hscontrol/types/common.go @@ -26,6 +26,7 @@ var ( ErrCannotParsePrefix = errors.New("cannot parse prefix") ErrInvalidAuthIDLength = errors.New("auth ID has invalid length") ErrInvalidAuthIDPrefix = errors.New("auth ID has invalid prefix") + ErrInvalidAuthIDChars = errors.New("auth ID has invalid characters") ErrRegistrationPayloadTooLarge = errors.New("registration payload exceeds size limit") ) @@ -81,6 +82,13 @@ func (r AuthID) Validate() error { ) } + for i := len(authIDPrefix); i < len(r); i++ { + c := r[i] + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { + return ErrInvalidAuthIDChars + } + } + return nil } diff --git a/hscontrol/types/common_test.go b/hscontrol/types/common_test.go index 67fe48417..9209c5086 100644 --- a/hscontrol/types/common_test.go +++ b/hscontrol/types/common_test.go @@ -305,3 +305,48 @@ func TestDefaultBatcherWorkers(t *testing.T) { t.Errorf("DefaultBatcherWorkers() = %d, expected value >= 1", result) } } + +func TestAuthIDFromStringRejectsNonHex(t *testing.T) { + generated := MustAuthID() + parsed, err := AuthIDFromString(generated.String()) + require.NoError(t, err) + require.Equal(t, generated, parsed) + + _, err = AuthIDFromString(authIDPrefix + "0123456789abcdef01234567") + require.NoError(t, err) + + _, err = AuthIDFromString("wrong-prefix" + strings.Repeat(";", authIDRandomLength)) + require.ErrorIs(t, err, ErrInvalidAuthIDPrefix) + + _, err = AuthIDFromString(authIDPrefix + ";") + require.ErrorIs(t, err, ErrInvalidAuthIDLength) + + for _, tt := range []struct { + name string + bad string + }{ + {name: "semicolon", bad: ";"}, + {name: "substitution", bad: "$()"}, + {name: "space", bad: " "}, + {name: "newline", bad: "\n"}, + {name: "uppercase", bad: "A"}, + {name: "beyond f", bad: "g"}, + {name: "underscore", bad: "_"}, + {name: "non-ASCII", bad: "é"}, + } { + for _, position := range []string{"start", "end"} { + t.Run(tt.name+"/"+position, func(t *testing.T) { + padding := strings.Repeat("a", authIDRandomLength-len(tt.bad)) + suffix := tt.bad + padding + + if position == "end" { + suffix = padding + tt.bad + } + + parsed, err := AuthIDFromString(authIDPrefix + suffix) + require.ErrorIs(t, err, ErrInvalidAuthIDChars) + require.Empty(t, parsed) + }) + } + } +}