fix(apiserver): match JWT-excluded paths exactly, wildcard only for /* (bug-1962)
Cause: isExcludedPath (middleware/validatehandler.go) dropped the last character of every excluded entry and prefix-matched, so /ping excused any path starting with /pin (e.g. /pinX) from JWT validation, while /swagger/*any (trimmed to /swagger/*an) never matched. Fix: entries match exactly; an entry whose last segment is a gin catch-all (/swagger/*any) matches any path under the part before the "*". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d720132c8c
commit
387a421c57
2 changed files with 76 additions and 14 deletions
|
|
@ -44,23 +44,30 @@ func ValidateHandler() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isExcludedPath(path string) bool {
|
// jwtExcludedPaths are the routes that skip JWT validation. An entry is
|
||||||
|
// matched exactly, unless its last segment is a gin catch-all ("*name", as in
|
||||||
|
// the route "/swagger/*any"): then any path under the part before the "*"
|
||||||
|
// matches (bug-1962).
|
||||||
|
var jwtExcludedPaths = []string{
|
||||||
|
"/ping",
|
||||||
|
"/xce/v1/auth/logon",
|
||||||
|
"/swagger/*any",
|
||||||
|
"/xce/v1/auth/logout",
|
||||||
|
}
|
||||||
|
|
||||||
allowed := false
|
func isExcludedPath(path string) bool {
|
||||||
// define some excluded paths
|
for _, excludedPath := range jwtExcludedPaths {
|
||||||
excludedPaths := []string{
|
if i := strings.LastIndex(excludedPath, "/*"); i >= 0 {
|
||||||
"/ping",
|
if strings.HasPrefix(path, excludedPath[:i+1]) {
|
||||||
"/xce/v1/auth/logon",
|
return true
|
||||||
"/swagger/*any",
|
}
|
||||||
"/xce/v1/auth/logout",
|
continue
|
||||||
}
|
}
|
||||||
for _, excludedPath := range excludedPaths {
|
if path == excludedPath {
|
||||||
if strings.HasPrefix(path, excludedPath[:len(excludedPath)-1]) { // 去掉最后的 *
|
return true
|
||||||
allowed = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allowed
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token validation middleware
|
// Token validation middleware
|
||||||
|
|
|
||||||
|
|
@ -150,3 +150,58 @@ func TestIsValidToken_PinsSigningAlgorithm(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bug-1962: exact excluded entries match only themselves; only the wildcard
|
||||||
|
// entry (/swagger/*any) matches by prefix.
|
||||||
|
func TestIsExcludedPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"/ping", true},
|
||||||
|
{"/xce/v1/auth/logon", true},
|
||||||
|
{"/xce/v1/auth/logout", true},
|
||||||
|
{"/swagger/index.html", true},
|
||||||
|
{"/swagger/doc.json", true},
|
||||||
|
// prefix look-alikes must not skip JWT validation
|
||||||
|
{"/pinX", false},
|
||||||
|
{"/pin", false},
|
||||||
|
{"/ping/x", false},
|
||||||
|
{"/pingpong", false},
|
||||||
|
{"/xce/v1/auth/logonX", false},
|
||||||
|
{"/xce/v1/auth/logo", false},
|
||||||
|
{"/xce/v1/auth/logout/x", false},
|
||||||
|
{"/swaggerX", false},
|
||||||
|
{"/xce/v1/jobs", false},
|
||||||
|
{"/", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := isExcludedPath(tt.path); got != tt.want {
|
||||||
|
t.Errorf("isExcludedPath(%q) = %v, want %v", tt.path, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bug-1962: a request to a path that only shares a prefix with an excluded
|
||||||
|
// entry must still be rejected without a token; the excluded entry itself
|
||||||
|
// still needs none.
|
||||||
|
func TestValidateHandler_PrefixLookAlikeNeedsToken(t *testing.T) {
|
||||||
|
setTestAuthConfig(t)
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(ValidateHandler())
|
||||||
|
r.GET("/pinX", func(ctx *gin.Context) { ctx.String(http.StatusOK, "reached") })
|
||||||
|
r.GET("/ping", func(ctx *gin.Context) { ctx.String(http.StatusOK, "pong") })
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/pinX", nil))
|
||||||
|
if strings.Contains(w.Body.String(), "reached") {
|
||||||
|
t.Errorf("/pinX without a token reached the handler (status %d)", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/ping", nil))
|
||||||
|
if w.Code != http.StatusOK || w.Body.String() != "pong" {
|
||||||
|
t.Errorf("/ping without a token: status %d body %q, want 200 pong", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue