GO.OPENTELEMETRY.IO/OTEL/SDK · CVE-2026-128085 · Package
FIX → 1.47.0
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.
first public Oct 28, 2026 · 2 sources
Check it with your coding agent
It reads this page and tells you if your code or app is affected.
Applications that use the OpenTelemetry Go SDK for tracing or logging and set an attribute value length limit, where attribute values can carry untrusted input made of U+FFFD characters. The flaw only changes how much data telemetry keeps; it does not allow code execution. Services that do not use these modules at these versions, or that never record attacker-influenced attributes, can skip it.
Not affected if default-config, no-untrusted-attributes
Who cares go, logging, opentelemetry, tracing
go.opentelemetry.io/otel/sdk, go.opentelemetry.io/otel/sdk/log and go.opentelemetry.io/otel/trace in go.mod or 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.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
InfraStack checks it against your stack and answers on your report, with the evidence and the fix.
go list -m allAttributeValueLengthLimit or WithAttributeValueLengthLimit in your code; the limit is only meaningful if it is setUpgrade to 1.47.0
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)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:
// 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:
count, so no truncation ever triggers and the value is returned unchanged, whatever its length.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):
span.SetAttributes -> sdk/trace/span.go (normAttr) -> attrnorm.Truncate -> sdk/internal/attrnorm/truncate.go (truncate)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.
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:
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
This page as .md · .json · CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.