From 877363a686c3b68c71eb64c70b5623fa6b0594cc Mon Sep 17 00:00:00 2001 From: Laurence Date: Mon, 2 Mar 2026 16:48:46 +0000 Subject: [PATCH 1/4] Strip session cookie and query param before forwarding to backend - Add stripSessionCookies() to remove session cookies from Cookie header - Add stripSessionParam() to remove session exchange query parameter from URL - Call both before forwarding request to backend on valid sessions - Backend now only receives user identity via Remote-* headers --- main.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/main.go b/main.go index d79253c..fd4bd52 100644 --- a/main.go +++ b/main.go @@ -312,6 +312,9 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) { req.Header.Add("Remote-Role", *result.Data.Role) } + p.stripSessionCookies(req) + p.stripSessionParam(req) + fmt.Println("Badger: Valid session") p.next.ServeHTTP(rw, req) return @@ -414,6 +417,44 @@ func (p *Badger) getRealIP(req *http.Request) string { return ip } +func (p *Badger) stripSessionParam(req *http.Request) { + query := req.URL.Query() + if query.Has(p.resourceSessionRequestParam) { + query.Del(p.resourceSessionRequestParam) + req.URL.RawQuery = query.Encode() + } +} + +// stripSessionCookies removes session cookies from the request before forwarding to the backend. +// We parse the Cookie header as a string rather than using req.Cookies() + re-encoding so we +// preserve the exact header format; the Cookie request header only carries name=value pairs +// (domain/path are Set-Cookie response attributes and are not sent in requests per RFC 6265). +func (p *Badger) stripSessionCookies(req *http.Request) { + cookieHeader := req.Header.Get("Cookie") + if cookieHeader == "" { + return + } + + var remaining []string + for part := range strings.SplitSeq(cookieHeader, ";") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + parts := strings.SplitN(part, "=", 2) + name := strings.TrimSpace(parts[0]) + if !strings.HasPrefix(name, p.userSessionCookieName) { + remaining = append(remaining, part) + } + } + + if len(remaining) > 0 { + req.Header.Set("Cookie", strings.Join(remaining, "; ")) + } else { + req.Header.Del("Cookie") + } +} + func (p *Badger) isTrustedIP(remoteAddr string) bool { ipStr, _, err := net.SplitHostPort(remoteAddr) if err != nil { From 2902340f7b276543865e91260193862adeb076db Mon Sep 17 00:00:00 2001 From: Laurence Date: Tue, 3 Mar 2026 17:51:16 +0000 Subject: [PATCH 2/4] fix: handle other fields and ensure cookie coverage --- main.go | 69 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index fd4bd52..5d151ae 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ type Config struct { APIBaseUrl string `json:"apiBaseUrl,omitempty"` UserSessionCookieName string `json:"userSessionCookieName,omitempty"` ResourceSessionRequestParam string `json:"resourceSessionRequestParam,omitempty"` + AccessTokenQueryParam string `json:"accessTokenQueryParam,omitempty"` // Deprecated: use ResourceSessionRequestParam DisableForwardAuth bool `json:"disableForwardAuth,omitempty"` TrustIP []string `json:"trustip,omitempty"` DisableDefaultCFIPs bool `json:"disableDefaultCFIPs,omitempty"` @@ -37,6 +38,7 @@ type Badger struct { apiBaseUrl string userSessionCookieName string resourceSessionRequestParam string + accessTokenQueryParam string disableForwardAuth bool trustIP []*net.IPNet customIPHeader string @@ -95,6 +97,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h apiBaseUrl: config.APIBaseUrl, userSessionCookieName: config.UserSessionCookieName, resourceSessionRequestParam: config.ResourceSessionRequestParam, + accessTokenQueryParam: config.AccessTokenQueryParam, disableForwardAuth: config.DisableForwardAuth, customIPHeader: config.CustomIPHeader, } @@ -314,6 +317,8 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) { p.stripSessionCookies(req) p.stripSessionParam(req) + req.Header.Del("P-Access-Token-Id") + req.Header.Del("P-Access-Token") fmt.Println("Badger: Valid session") p.next.ServeHTTP(rw, req) @@ -419,39 +424,69 @@ func (p *Badger) getRealIP(req *http.Request) string { func (p *Badger) stripSessionParam(req *http.Request) { query := req.URL.Query() + modified := false if query.Has(p.resourceSessionRequestParam) { query.Del(p.resourceSessionRequestParam) + modified = true + } + if p.accessTokenQueryParam != "" && query.Has(p.accessTokenQueryParam) { + query.Del(p.accessTokenQueryParam) + modified = true + } + if modified { req.URL.RawQuery = query.Encode() + req.RequestURI = req.URL.RequestURI() } } // stripSessionCookies removes session cookies from the request before forwarding to the backend. -// We parse the Cookie header as a string rather than using req.Cookies() + re-encoding so we -// preserve the exact header format; the Cookie request header only carries name=value pairs -// (domain/path are Set-Cookie response attributes and are not sent in requests per RFC 6265). +// Cookie request headers only contain name=value pairs (Set-Cookie attributes like Path/Domain +// are response-only), so we filter parsed request cookies and rebuild the Cookie header. func (p *Badger) stripSessionCookies(req *http.Request) { - cookieHeader := req.Header.Get("Cookie") - if cookieHeader == "" { + cookieHeaders := req.Header.Values("Cookie") + if len(cookieHeaders) == 0 { return } - var remaining []string - for part := range strings.SplitSeq(cookieHeader, ";") { - part = strings.TrimSpace(part) - if part == "" { + var remaining []*http.Cookie + for _, headerValue := range cookieHeaders { + parsedCookies, err := http.ParseCookie(headerValue) + if err != nil { + // Best-effort fallback for malformed Cookie headers. + for _, part := range strings.Split(headerValue, ";") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + name, value, hasValue := strings.Cut(part, "=") + name = strings.TrimSpace(name) + if strings.HasPrefix(name, p.userSessionCookieName) { + continue + } + if hasValue { + remaining = append(remaining, &http.Cookie{ + Name: name, + Value: strings.TrimSpace(value), + }) + } + } continue } - parts := strings.SplitN(part, "=", 2) - name := strings.TrimSpace(parts[0]) - if !strings.HasPrefix(name, p.userSessionCookieName) { - remaining = append(remaining, part) + + for _, cookie := range parsedCookies { + if !strings.HasPrefix(cookie.Name, p.userSessionCookieName) { + remaining = append(remaining, cookie) + } } } - if len(remaining) > 0 { - req.Header.Set("Cookie", strings.Join(remaining, "; ")) - } else { - req.Header.Del("Cookie") + req.Header.Del("Cookie") + if len(remaining) == 0 { + return + } + + for _, cookie := range remaining { + req.AddCookie(cookie) } } From d81af67db37529e55389f71270c27981f52107b7 Mon Sep 17 00:00:00 2001 From: Laurence Date: Tue, 3 Mar 2026 19:03:08 +0000 Subject: [PATCH 3/4] fix: traefik doesnt know cookies helpers --- main.go | 47 ++++++++++++++--------------------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/main.go b/main.go index 5d151ae..0cf2d92 100644 --- a/main.go +++ b/main.go @@ -440,54 +440,35 @@ func (p *Badger) stripSessionParam(req *http.Request) { } // stripSessionCookies removes session cookies from the request before forwarding to the backend. -// Cookie request headers only contain name=value pairs (Set-Cookie attributes like Path/Domain -// are response-only), so we filter parsed request cookies and rebuild the Cookie header. +// It processes raw Cookie header pairs so non-target cookies are preserved as-is. func (p *Badger) stripSessionCookies(req *http.Request) { cookieHeaders := req.Header.Values("Cookie") if len(cookieHeaders) == 0 { return } - var remaining []*http.Cookie + var remainingPairs []string for _, headerValue := range cookieHeaders { - parsedCookies, err := http.ParseCookie(headerValue) - if err != nil { - // Best-effort fallback for malformed Cookie headers. - for _, part := range strings.Split(headerValue, ";") { - part = strings.TrimSpace(part) - if part == "" { - continue - } - name, value, hasValue := strings.Cut(part, "=") - name = strings.TrimSpace(name) - if strings.HasPrefix(name, p.userSessionCookieName) { - continue - } - if hasValue { - remaining = append(remaining, &http.Cookie{ - Name: name, - Value: strings.TrimSpace(value), - }) - } + for _, part := range strings.Split(headerValue, ";") { + part = strings.TrimSpace(part) + if part == "" { + continue } - continue - } - - for _, cookie := range parsedCookies { - if !strings.HasPrefix(cookie.Name, p.userSessionCookieName) { - remaining = append(remaining, cookie) + name, _, _ := strings.Cut(part, "=") + name = strings.TrimSpace(name) + if !strings.HasPrefix(name, p.userSessionCookieName) { + remainingPairs = append(remainingPairs, part) } } } - req.Header.Del("Cookie") - if len(remaining) == 0 { + if len(remainingPairs) == 0 { + req.Header.Del("Cookie") return } - for _, cookie := range remaining { - req.AddCookie(cookie) - } + // Keep a single canonical Cookie header while preserving surviving name=value pairs. + req.Header.Set("Cookie", strings.Join(remainingPairs, "; ")) } func (p *Badger) isTrustedIP(remoteAddr string) bool { From f220c75a52f0654c7a77f51282ca0b1056f3d6c5 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 17 Mar 2026 16:59:32 -0700 Subject: [PATCH 4/4] use access token params from config --- README.md | 14 -------------- main.go | 20 +++++++++++++++++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 58fe41c..88e7d55 100644 --- a/README.md +++ b/README.md @@ -70,20 +70,6 @@ trustip: customIPHeader: "X-Forwarded-For" ``` -### Configuration Options Reference - -| Option | Type | Required\* | Default | Description | -| ----------------------------- | -------- | ---------- | ------- | ----------------------------------------------------------------------------------- | -| `disableForwardAuth` | bool | No | `false` | Disable forward auth; only IP handling is performed | -| `apiBaseUrl` | string | Yes\* | - | Base URL of the Pangolin API | -| `userSessionCookieName` | string | Yes\* | - | Cookie name for user sessions | -| `resourceSessionRequestParam` | string | Yes\* | - | Query parameter name for resource session requests | -| `trustip` | []string | No | `[]` | Array of trusted IP ranges in CIDR format | -| `disableDefaultCFIPs` | bool | No | `false` | Disable default Cloudflare IP ranges | -| `customIPHeader` | string | No | `""` | Custom header name to extract IP from (only used if request is from trusted source) | - -\* Required only when `disableForwardAuth` is `false` (default) - ## Updating Cloudflare IPs To update the Cloudflare IP ranges, run: diff --git a/main.go b/main.go index 0cf2d92..0258e1c 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,9 @@ type Config struct { APIBaseUrl string `json:"apiBaseUrl,omitempty"` UserSessionCookieName string `json:"userSessionCookieName,omitempty"` ResourceSessionRequestParam string `json:"resourceSessionRequestParam,omitempty"` - AccessTokenQueryParam string `json:"accessTokenQueryParam,omitempty"` // Deprecated: use ResourceSessionRequestParam + AccessTokenQueryParam string `json:"accessTokenQueryParam,omitempty"` + AccessTokenIDHeader string `json:"accessTokenIdHeader,omitempty"` + AccessTokenHeader string `json:"accessTokenHeader,omitempty"` DisableForwardAuth bool `json:"disableForwardAuth,omitempty"` TrustIP []string `json:"trustip,omitempty"` DisableDefaultCFIPs bool `json:"disableDefaultCFIPs,omitempty"` @@ -39,6 +41,8 @@ type Badger struct { userSessionCookieName string resourceSessionRequestParam string accessTokenQueryParam string + accessTokenIDHeader string + accessTokenHeader string disableForwardAuth bool trustIP []*net.IPNet customIPHeader string @@ -98,6 +102,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h userSessionCookieName: config.UserSessionCookieName, resourceSessionRequestParam: config.ResourceSessionRequestParam, accessTokenQueryParam: config.AccessTokenQueryParam, + accessTokenIDHeader: config.AccessTokenIDHeader, + accessTokenHeader: config.AccessTokenHeader, disableForwardAuth: config.DisableForwardAuth, customIPHeader: config.CustomIPHeader, } @@ -317,8 +323,7 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) { p.stripSessionCookies(req) p.stripSessionParam(req) - req.Header.Del("P-Access-Token-Id") - req.Header.Del("P-Access-Token") + p.stripAccessTokenHeaders(req) fmt.Println("Badger: Valid session") p.next.ServeHTTP(rw, req) @@ -439,6 +444,15 @@ func (p *Badger) stripSessionParam(req *http.Request) { } } +func (p *Badger) stripAccessTokenHeaders(req *http.Request) { + if p.accessTokenIDHeader != "" { + req.Header.Del(p.accessTokenIDHeader) + } + if p.accessTokenHeader != "" { + req.Header.Del(p.accessTokenHeader) + } +} + // stripSessionCookies removes session cookies from the request before forwarding to the backend. // It processes raw Cookie header pairs so non-target cookies are preserved as-is. func (p *Badger) stripSessionCookies(req *http.Request) {