From 28188127dd4392e278623d148d23eafdc0c2128d Mon Sep 17 00:00:00 2001 From: Gray Fay Date: Sat, 12 Sep 2026 01:09:59 -0400 Subject: [PATCH 1/2] chore: clear all reachable vulnerabilities and check for them in CI govulncheck reported three vulnerabilities reachable from this code. Nothing was scanning, so they sat there: a version number does not announce itself. GO-2026-6293 echo v4.11.3 -- an encoded slash (%2F) bypasses route-level protection and exposes static files. Reachable from the e.Static call in main.go. Fixed in v4.15.3. GO-2025-3553 golang-jwt/jwt v3 -- memory exhaustion parsing headers. "Fixed in: N/A" -- v3 is unmaintained, so the only way out is v5. GO-2024-2687 golang.org/x/net v0.17.0 -- HTTP/2 CONTINUATION flood. Fixed in v0.23.0. govulncheck now reports zero. The jwt migration is the only one with real API change: StandardClaims becomes RegisteredClaims, holding exp and iat as NumericDate rather than int64. The wire format is identical -- both marshal the same JSON numbers -- so tokens signed before the upgrade keep validating and the deploy does not log everyone out mid-session. Asserted rather than assumed: a test builds a token the way v3 built one and checks it still parses. v5 also validates exp by default, which v3 did not do unless asked, and WithValidMethods makes the algorithm check an enforced parse option rather than a test the keyfunc has to remember. Tests cover expiry, alg=none confusion and a wrong signing key. go.mod said 1.21 while the image built on 1.24, so the enforced language version was never the shipped one. Current x/crypto and x/net require 1.26, so both are now 1.26 and the builder image moves with it -- verified by building the backend stage, since a directive the builder cannot satisfy breaks the image rather than the tests. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 10 ++++ Dockerfile | 2 +- go.mod | 24 ++++---- go.sum | 58 +++++++----------- services/auth_service.go | 26 +++++--- services/jwt_compat_test.go | 115 ++++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 services/jwt_compat_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b81f773..3b6a336 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,16 @@ jobs: - name: Vet run: go vet ./... + # Nothing was checking this, and it had rotted badly: echo carried an + # encoded-slash bug exposing static files, golang-jwt v3 an unfixable + # memory-exhaustion bug, x/net the HTTP/2 CONTINUATION flood. All three + # were reachable from this code, and all three sat there for months + # because a version number does not announce itself. + - name: Vulnerabilities + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + "$(go env GOPATH)/bin/govulncheck" ./... + # Tests that need Postgres skip themselves when nothing is listening, so # this stays fast without a service container. The integration probes # additionally require PROBE_DSN and are skipped here by design. diff --git a/Dockerfile b/Dockerfile index 7aa80f6..1b2c881 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY frontend/ ./ RUN bun run build # Backend build stage -FROM golang:1.24-alpine AS backend-builder +FROM golang:1.26-alpine AS backend-builder # Install build dependencies RUN apk add --no-cache git ca-certificates tzdata diff --git a/go.mod b/go.mod index 7e0ed5a..923f356 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,27 @@ module geocoding-api -go 1.21 +go 1.26.0 require ( - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.11.3 + github.com/labstack/echo/v4 v4.15.4 github.com/lib/pq v1.10.9 - github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.14.0 - golang.org/x/time v0.3.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.57.0 + golang.org/x/time v0.15.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/labstack/gommon v0.4.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/labstack/gommon v0.5.0 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3e36731..8c2024b 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,38 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= -github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.15.4 h1:DL45vVYa+BWE+XuW+zZNd9H0YEdZ80UAWJGcTVW4EVs= +github.com/labstack/echo/v4 v4.15.4/go.mod h1:CuMetKIRwsuO/qlAgMq+KTAalwGoB/h4tC+yPdrTj1g= +github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= +github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/services/auth_service.go b/services/auth_service.go index c274419..445b516 100644 --- a/services/auth_service.go +++ b/services/auth_service.go @@ -15,7 +15,7 @@ import ( "geocoding-api/database" "geocoding-api/models" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/lib/pq" "golang.org/x/crypto/bcrypt" ) @@ -23,12 +23,21 @@ import ( // AuthService handles authentication and API key management type AuthService struct{} -// JWTClaims represents the JWT token claims +// JWTClaims represents the JWT token claims. +// +// RegisteredClaims rather than v3's StandardClaims: golang-jwt v3 is +// unmaintained and carries GO-2025-3553 (memory exhaustion parsing headers) +// with no fix available, so the only way out was v5. +// +// The wire format is unchanged. RegisteredClaims marshals the same "exp" and +// "iat" JSON fields StandardClaims did -- it differs only in holding them as +// NumericDate rather than int64 -- so tokens signed before this upgrade keep +// validating and nobody is logged out by the deploy. type JWTClaims struct { UserID int `json:"user_id"` Email string `json:"email"` IsAdmin bool `json:"is_admin"` - jwt.StandardClaims + jwt.RegisteredClaims } // GenerateJWT creates a new JWT token for a user @@ -44,9 +53,9 @@ func (as *AuthService) GenerateJWT(user *models.User) (string, error) { UserID: user.ID, Email: user.Email, IsAdmin: user.IsAdmin, - StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), // Token expires in 24 hours - IssuedAt: time.Now().Unix(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Token expires in 24 hours + IssuedAt: jwt.NewNumericDate(time.Now()), }, } @@ -71,13 +80,16 @@ func (as *AuthService) ValidateJWT(tokenString string) (*JWTClaims, error) { } // Parse token + // WithValidMethods makes the algorithm check an enforced parse option + // rather than only a test inside the keyfunc. v5 also validates exp and iat + // by default, which v3 did not do unless asked. token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { // Validate signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(secret), nil - }) + }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) if err != nil { return nil, err diff --git a/services/jwt_compat_test.go b/services/jwt_compat_test.go new file mode 100644 index 0000000..dc415c5 --- /dev/null +++ b/services/jwt_compat_test.go @@ -0,0 +1,115 @@ +package services + +import ( + "testing" + "time" + + "geocoding-api/models" + + "github.com/golang-jwt/jwt/v5" +) + +// A token signed before the v3 -> v5 upgrade must still validate afterwards. +// +// v5 stores exp and iat as NumericDate where v3 used int64, but both marshal +// to the same JSON numbers, so the wire format is unchanged. If that were +// wrong, the deploy would invalidate every session in flight and log out every +// signed-in user at once -- so it is asserted rather than assumed. The token +// below is built the way v3 built one: raw numeric claims, no NumericDate. +func TestTokensSignedByTheOldLibraryStillValidate(t *testing.T) { + t.Setenv("JWT_SECRET", "compat-test-secret") + + legacy := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": 42, + "email": "legacy@example.com", + "is_admin": true, + "exp": time.Now().Add(time.Hour).Unix(), + "iat": time.Now().Add(-time.Minute).Unix(), + }) + tokenString, err := legacy.SignedString([]byte("compat-test-secret")) + if err != nil { + t.Fatalf("sign legacy token: %v", err) + } + + claims, err := Auth.ValidateJWT(tokenString) + if err != nil { + t.Fatalf("a token in the pre-upgrade format was rejected: %v", err) + } + if claims.UserID != 42 || claims.Email != "legacy@example.com" || !claims.IsAdmin { + t.Errorf("claims did not survive the round trip: %+v", claims) + } +} + +// Round trip through the new code path. +func TestGeneratedTokenValidates(t *testing.T) { + t.Setenv("JWT_SECRET", "compat-test-secret") + + token, err := Auth.GenerateJWT(&models.User{ID: 7, Email: "user@example.com", IsAdmin: false}) + if err != nil { + t.Fatalf("generate: %v", err) + } + claims, err := Auth.ValidateJWT(token) + if err != nil { + t.Fatalf("validate: %v", err) + } + if claims.UserID != 7 || claims.Email != "user@example.com" || claims.IsAdmin { + t.Errorf("unexpected claims: %+v", claims) + } +} + +// v3 did not check expiry unless asked; v5 does it by default. Confirm an +// expired token is actually refused rather than silently accepted. +func TestExpiredTokenIsRejected(t *testing.T) { + t.Setenv("JWT_SECRET", "compat-test-secret") + + expired := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": 1, + "email": "expired@example.com", + "exp": time.Now().Add(-time.Hour).Unix(), + "iat": time.Now().Add(-2 * time.Hour).Unix(), + }) + tokenString, _ := expired.SignedString([]byte("compat-test-secret")) + + if _, err := Auth.ValidateJWT(tokenString); err == nil { + t.Error("an expired token was accepted") + } +} + +// The classic JWT attack: swap the algorithm to one the verifier will accept +// with a key it should not use. WithValidMethods makes the refusal a parse +// option rather than a check the keyfunc has to remember to perform. +func TestAlgorithmConfusionIsRejected(t *testing.T) { + t.Setenv("JWT_SECRET", "compat-test-secret") + + none := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{ + "user_id": 1, + "email": "attacker@example.com", + "is_admin": true, + "exp": time.Now().Add(time.Hour).Unix(), + }) + tokenString, err := none.SignedString(jwt.UnsafeAllowNoneSignatureType) + if err != nil { + t.Fatalf("sign alg=none token: %v", err) + } + + if _, err := Auth.ValidateJWT(tokenString); err == nil { + t.Error("a token signed with alg=none was accepted") + } +} + +// A token signed with the wrong key must fail regardless of how well formed it +// is otherwise. +func TestWrongSecretIsRejected(t *testing.T) { + t.Setenv("JWT_SECRET", "compat-test-secret") + + forged := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": 1, + "is_admin": true, + "exp": time.Now().Add(time.Hour).Unix(), + }) + tokenString, _ := forged.SignedString([]byte("not-the-real-secret")) + + if _, err := Auth.ValidateJWT(tokenString); err == nil { + t.Error("a token signed with the wrong secret was accepted") + } +} From b0dd9beed3dc1ac39443bfcbd846f251d6593e77 Mon Sep 17 00:00:00 2001 From: Gray Fay Date: Sat, 12 Sep 2026 01:12:27 -0400 Subject: [PATCH 2/2] fix: pin a Go toolchain whose standard library is patched The new CI step failed on its first run, correctly. go-version-file pins the toolchain from the go directive, and the Go 1.26.0 standard library carries 20 known vulnerabilities fixed in 1.26.1 -- including two reachable through echo.StartServer, in crypto/x509 name-constraint checking. Worth noting how it was missed locally: govulncheck reports the standard library of whatever toolchain runs it, and mine is 1.27.1, so a local scan came back clean while the version CI would actually build with did not. The dependency upgrades were the visible problem; the toolchain was the one only CI could see. Both go.mod and the builder image are now 1.27. Co-Authored-By: Claude Opus 5 --- Dockerfile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1b2c881..198cb10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY frontend/ ./ RUN bun run build # Backend build stage -FROM golang:1.26-alpine AS backend-builder +FROM golang:1.27-alpine AS backend-builder # Install build dependencies RUN apk add --no-cache git ca-certificates tzdata diff --git a/go.mod b/go.mod index 923f356..5a164f3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module geocoding-api -go 1.26.0 +go 1.27.0 require ( github.com/golang-jwt/jwt/v5 v5.3.1