---
id: "CVE-2026-128085"
page: "https://infrastack.ai/live/CVE-2026-128085"
kind: "vulnerability"
aliases: ["CVE-2026-128085", "GHSA-q7gw-24fr-5p69"]
title: "opentelemetry-go: AttributeValueLengthLimit is fully bypassed by attribute values composed of valid U+FFFD runes"
severity: "medium"
cvss: 5.3
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "go.opentelemetry.io/otel/sdk", "fixed": ["1.47.0"], "affected": ">= 1.33.0, < 1.47.0", "ecosystem": "go"}, {"name": "go.opentelemetry.io/otel/sdk/log", "fixed": ["1.47.0"], "affected": ">= 0.9.0, < 1.47.0", "ecosystem": "go"}, {"name": "go.opentelemetry.io/otel/trace", "fixed": ["1.47.0"], "affected": ">= 1.35.0, < 1.47.0", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["cwe-135", "cwe-770", "go", "logging", "opentelemetry", "package", "tracing"]
not_affected_if: ["default-config", "no-untrusted-attributes"]
published_at: "2026-11-13T19:00:55+00:00"
first_seen_at: "2026-11-13T19:03:28+00:00"
updated_at: "2026-11-13T19:03:57+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-q7gw-24fr-5p69", "first_seen_at": "2026-11-13T19:03:28+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-128085", "first_seen_at": "2026-11-13T19:52:09+00:00"}]
exploit_sources: []
---

# opentelemetry-go: AttributeValueLengthLimit is fully bypassed by attribute values composed of valid U+FFFD runes

Link for people: https://infrastack.ai/live/CVE-2026-128085 (this .md is for agents)

The OpenTelemetry Go SDK's attribute value length limit does not count valid U+FFFD (replacement character) runes, so attribute values made of them are stored untruncated. An attacker who controls span or log attribute values can make telemetry store oversized data, which can exhaust memory or storage in the telemetry pipeline.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:go.opentelemetry.io/otel/sdk affected >= 1.33.0, < 1.47.0 fixed 1.47.0
- go:go.opentelemetry.io/otel/sdk/log affected >= 0.9.0, < 1.47.0 fixed 1.47.0
- go:go.opentelemetry.io/otel/trace affected >= 1.35.0, < 1.47.0 fixed 1.47.0

## How to check

- Check the version of `go.opentelemetry.io/otel/sdk`, `go.opentelemetry.io/otel/sdk/log` and `go.opentelemetry.io/otel/trace` in `go.mod` or `go list -m all`
- Look for `AttributeValueLengthLimit` or `WithAttributeValueLengthLimit` in your code; the limit is only meaningful if it is set
- Confirm whether untrusted input (request fields, headers) ends up in span or log attributes

## Fix

- `go.opentelemetry.io/otel/sdk`: `1.47.0` (affected `>= 1.33.0`, `< 1.47.0`)
- `go.opentelemetry.io/otel/sdk/log`: `1.47.0` (affected `>= 0.9.0`, `< 1.47.0`)
- `go.opentelemetry.io/otel/trace`: `1.47.0` (affected `>= 1.35.0`, `< 1.47.0`)

## Timeline

- 2026-11-13T19:03:28+00:00 ghsa https://github.com/advisories/GHSA-q7gw-24fr-5p69
- 2026-11-13T19:52:09+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-128085

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/open-telemetry/opentelemetry-go/security-advisories/GHSA-q7gw-24fr-5p69
- [advisory] Maintainer's advisory: github.com https://github.com/open-telemetry/opentelemetry-go/security/advisories/GHSA-q7gw-24fr-5p69
- [fix] Patch: github.com https://github.com/open-telemetry/opentelemetry-go/commit/017b08c7f1018b619991aaf6b2299d4beb9da1d3
- [fix] Release: github.com https://github.com/open-telemetry/opentelemetry-go/releases/tag/sdk/log/v1.47.0
- [fix] Release: github.com https://github.com/open-telemetry/opentelemetry-go/releases/tag/sdk/v1.47.0
- [fix] Release: github.com https://github.com/open-telemetry/opentelemetry-go/releases/tag/trace/v1.47.0

## From the sources

## Summary

The `AttributeValueLengthLimit` span/log-record limit can be bypassed entirely with attribute values that consist of valid U+FFFD (REPLACEMENT CHARACTER) runes. The counting-based truncation introduced by the fix for GHSA-p9f8-wvj8-2fg8 (commit `e016a78c9`, first released in v1.33.0) has an "optimistic" fast path that skips every U+FFFD without counting it and without falling back to the correct slow path. A value of 1000 U+FFFD runes (3000 bytes) is stored verbatim even with a limit of 5 characters (15 bytes). The same defect affects the attribute truncation paths in `go.opentelemetry.io/otel/sdk/trace` (`sdk/internal/attrnorm`), `go.opentelemetry.io/otel/sdk/log` (`sdk/log/internal/attrnorm`), and the auto-instrumentation tracer in `go.opentelemetry.io/otel/trace` (`trace/auto.go`, introduced in `v1.35.0`).

This is an incomplete-fix issue: it is a distinct defect in the replacement code that GHSA-p9f8-wvj8-2fg8 introduced, affecting exactly the versions that advisory says are fixed (>= v1.33.0). The original advisory covers only the pre-v1.33.0 code.

## Details

The shared truncation helper is generated from `internal/shared/attrnorm/truncate.go.tmpl`. In the v1.46.0 release, the generated copies are `sdk/internal/attrnorm/truncate.go` and `sdk/log/internal/attrnorm/truncate.go` (identical logic, different package names). The optimistic path (v1.46.0, `sdk/internal/attrnorm/truncate.go:175-199`) is:

```go
// Optimistically, assume all valid UTF-8.
var b strings.Builder
count := 0
for i, c := range s {
    if c != utf8.RuneError {
        count++
        if count > limit {
            return s[:i]
        }
        continue
    }
    _, size := utf8.DecodeRuneInString(s[i:])
    if size == 1 {
        // invalid encoding: fall through to the correct slow path
        ...
        break
    }
}
```

`range` over a string yields `utf8.RuneError` both for invalid bytes (size 1) and for a legitimately encoded U+FFFD (size 3). Only the size-1 case enters the slow path. A valid U+FFFD rune is therefore neither counted nor truncating: the loop slides over it. Consequences:

1. A value made only of valid U+FFFD runes never increments `count`, so no truncation ever triggers and the value is returned unchanged, whatever its length.
2. In a mixed string, every U+FFFD contributes 3 bytes that are not counted, and the final `return s[:i]` keeps all previously skipped U+FFFD runes, so the retained size grows linearly with the number of U+FFFD runes and is not bounded by `limit`.

The OpenTelemetry specification states that truncation applies "counting any character in it as 1". A valid U+FFFD is one character and must count as 1; here it counts as 0. The regression test suite of the repository contains no U+FFFD case, which is why this went unnoticed.

Call chains (v1.46.0):

- trace: `span.SetAttributes` -> `sdk/trace/span.go` (`normAttr`) -> `attrnorm.Truncate` -> `sdk/internal/attrnorm/truncate.go` (`truncate`)
- log: `Record.AddAttributes` / `SetAttributes` -> `sdk/log/record.go` (`attrnorm.TruncateValue`) -> `sdk/log/internal/attrnorm/truncate.go` (`truncate`)

Deduplication: GHSA-p9f8-wvj8-2fg8 describes the pre-v1.33.0 helper (`safeTruncateValidUTF8`), which gave up truncating entirely for strings containing RuneError. The rewrite in `e016a78c9` replaced that code; this report concerns the rewritten code, is present only in versions that the advisory lists as fixed, and no public issue or advisory describes it.

## PoC

Minimal reproduction with the published release (no repository checkout needed). Go 1.25+ is required (the release declares `go 1.25.0`; any recent toolchain auto-selects one).

Create a directory with these files.

`go.mod` (then run `go mod tidy` once to fill `go.sum`):

```
module otelpoc

go 1.25.0

require (
	go.opentelemetry.io/otel v1.46.0
	go.opentelemetry.io/otel/log v0.22.0
	go.opentelemetry.io/otel/sdk v1.46.0
	go.opentelemetry.io/otel/sdk/log v0.22.0
	go.opentelemetry.io/otel/trace v1.46.0
)
```

`otelgo_truncate_test.go`:

```go
package otelpoc

import (
	"context"
	"strings"
	"testing"
	"unicode/utf8"

	"go.opentelemetry.io/otel/attribute"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/sdk/trace/tracetest"
	"go.opentelemetry.io/otel/trace"
)

// limitsWithLen returns span limits with AttributeValueLengthLimit=limit and
// everything else unlimited, isolating the length-limit enforcement under test.
func limitsWithLen(limit int) sdktrace.SpanLimits {
	lim := sdktrace.NewSpanLimits()
	lim.AttributeValueLengthLimit = limit
	lim.AttributeCountLimit = -1
	lim.AttributePerEventCountLimit = -1
	lim.AttributePerLinkCountLimit = -1
	lim.EventCountLimit = -1
	lim.LinkCountLimit = -1
	return lim
}

func captureOneSpan(t *testing.T, limit int, attrs ...attribute.KeyValue) []attribute.KeyValue {
	t.Helper()
	sr := tracetest.NewSpanRecorder()
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSpanProcessor(sr),
		sdktrace.WithSpanLimits(limitsWithLen(limit)),
	)
	defer func() { _ = tp.Shutdown(context.Background()) }()

	tr := tp.Tracer("poc")
	_, span := tr.Start(context.Background(), "op")
	span.SetAttributes(attrs...)
	span.End()

	ended := sr.Ended()
	if len(ended) != 1 {
		t.Fatalf("expected 1 ended span, got %d", len(ended))
	}
	return ended[0].Attributes()
}

// TestOT001_FFFDOnlyStringBypassesTraceLengthLimit demonstrates that a string
// attribute composed of valid U+FFFD runes is stored completely untruncated
// although AttributeValueLengthLimit=5.
func TestOT001_FFFDOnlyStringBypassesTraceLengthLimit(t *testing.T) {
	const limit = 5
	val := strings.Repeat("\uFFFD", 1000) // 1000 chars, 3000 bytes

	got := captureOneSpan(t, limit, attribute.String("k", val))
	if len(got) != 1 {
		t.Fatalf("expected 1 attribute, got %d", len(got))
	}
	stored := got[0].Value.AsString()
	t.Logf("input: %d chars / %d bytes; stored: %d chars / %d bytes; limit=%d",
		utf8.RuneCountInString(val), len(val),
		utf8.RuneCountInString(stored), len(stored), limit)
	// Per OTel spec ("counting any character in it as 1") the stored value
	// must be at most `limit` characters (<= 15 bytes for U+FFFD).
	if maxBytes := 3 * limit; len(stored) > maxBytes {
		t.Fatalf("BYPASS: AttributeValueLengthLimit=%d but %d bytes stored (expected <=%d)",
			limit, len(stored), maxBytes)
	}
}

// TestOT001_FFFDMixedStringOverRetains demonstrates that valid U+FFFD runes
// are not counted in the optimistic truncation path: every U+FFFD adds 3
// uncounted bytes to the retained value.
func TestOT001_FFFDMixedStringOverRetains(t *testing.T) {
	const limit = 5
	val := strings.Repeat("\uFFFD", 100) + strings.Repeat("A", 6) // 106 chars, 306 bytes

	got := captureOneSpan(t, limit, attribute.String("k", val))
	stored := got[0].Value.AsString()
	t.Logf("input: %d chars / %d bytes; stored: %d chars / %d bytes; limit=%d",
		utf8.RuneCountInString(val), len(val),
		utf8.RuneCountInString(stored), len(stored), limit)
	if chars := utf8.RuneCountInString(stored); chars > limit {
		t.Fatalf("BYPASS: AttributeValueLengthLimit=%d but %d chars / %d bytes stored",
			limit, chars, len(stored))
	}
}

// TestOT001_ASCIIControl is the control case: plain ASCII over-limit values
// ARE truncated, proving the length limit is active in the same setup.
func TestOT001_ASCIIControl(t *testing.T) {
	const limit = 5
	val := strings.Repeat("B", 20)

	got := captureOneSpan(t, limit, attribute.String("k", val))
	stored := got[0].Value.AsString()
	t.Logf("input: %d chars / %d bytes; stored: %d chars / %d bytes; limit=%d",
		utf8.RuneCountInString(val), len(val),
		utf8.RuneCountInString(stored), len(stored), limit)
	if len(stored) != limit {
		t.Fatalf("control failed: ASCII value not truncated (stored %d bytes)", len(stored))
	}
}

// TestOT001_EventAttributesNotLengthTruncated documents that span-event
// attributes bypass AttributeValueLengthLimit entirely (no attrnorm.Truncate
// call in recordingSpan.addEvent).
func TestOT001_EventAttributesNotLengthTruncated(t *te

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
