Skip to content

Commit ca4b0e0

Browse files
Improve SentryTraceHeader constructor parameter validation (#4604)
* Improve SentryTraceHeader constructor parameter validation * Reject malformed header with trailing slash and add test case * Make sentry-header regex private and final
1 parent 72b3637 commit ca4b0e0

File tree

2 files changed

+103
-12
lines changed

2 files changed

+103
-12
lines changed

sentry/src/main/java/io/sentry/SentryTraceHeader.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import io.sentry.exception.InvalidSentryTraceHeaderException;
44
import io.sentry.protocol.SentryId;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
57
import org.jetbrains.annotations.NotNull;
68
import org.jetbrains.annotations.Nullable;
79

@@ -13,6 +15,11 @@ public final class SentryTraceHeader {
1315
private final @NotNull SpanId spanId;
1416
private final @Nullable Boolean sampled;
1517

18+
private static final Pattern SENTRY_TRACEPARENT_HEADER_REGEX =
19+
Pattern.compile(
20+
"^[ \\t]*(?<traceId>[0-9a-f]{32})-(?<spanId>[0-9a-f]{16})(?<sampled>-[01])?[ \\t]*$",
21+
Pattern.CASE_INSENSITIVE);
22+
1623
public SentryTraceHeader(
1724
final @NotNull SentryId traceId,
1825
final @NotNull SpanId spanId,
@@ -23,20 +30,17 @@ public SentryTraceHeader(
2330
}
2431

2532
public SentryTraceHeader(final @NotNull String value) throws InvalidSentryTraceHeaderException {
26-
final String[] parts = value.split("-", -1);
27-
if (parts.length < 2) {
33+
Matcher matcher = SENTRY_TRACEPARENT_HEADER_REGEX.matcher(value);
34+
boolean matchesExist = matcher.matches();
35+
36+
if (!matchesExist || matcher.group("traceId") == null || matcher.group("spanId") == null) {
2837
throw new InvalidSentryTraceHeaderException(value);
29-
} else if (parts.length == 3) {
30-
this.sampled = "1".equals(parts[2]);
31-
} else {
32-
this.sampled = null;
33-
}
34-
try {
35-
this.traceId = new SentryId(parts[0]);
36-
this.spanId = new SpanId(parts[1]);
37-
} catch (Throwable e) {
38-
throw new InvalidSentryTraceHeaderException(value, e);
3938
}
39+
40+
this.traceId = new SentryId(matcher.group("traceId"));
41+
this.spanId = new SpanId(matcher.group("spanId"));
42+
this.sampled =
43+
matcher.group("sampled") == null ? null : "1".equals(matcher.group("sampled").substring(1));
4044
}
4145

4246
public @NotNull String getName() {

sentry/src/test/java/io/sentry/SentryTraceHeaderTest.kt

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kotlin.test.Test
66
import kotlin.test.assertEquals
77
import kotlin.test.assertFailsWith
88
import kotlin.test.assertNull
9+
import kotlin.text.substring
910

1011
class SentryTraceHeaderTest {
1112
@Test
@@ -15,6 +16,92 @@ class SentryTraceHeaderTest {
1516
assertEquals("sentry-trace header does not conform to expected format: $sentryId", ex.message)
1617
}
1718

19+
@Test
20+
fun `when there is a trailing dash without sampling decision throws exception`() {
21+
val sentryId = SentryId()
22+
val spanId = SpanId()
23+
val ex =
24+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId-") }
25+
assertEquals(
26+
"sentry-trace header does not conform to expected format: $sentryId-$spanId-",
27+
ex.message,
28+
)
29+
}
30+
31+
@Test
32+
fun `when trace-id has less than 32 characters throws exception`() {
33+
val sentryId = SentryId().toString().substring(0, 8)
34+
val spanId = SpanId()
35+
val ex =
36+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId") }
37+
assertEquals(
38+
"sentry-trace header does not conform to expected format: $sentryId-$spanId",
39+
ex.message,
40+
)
41+
}
42+
43+
@Test
44+
fun `when trace-id has more than 32 characters throws exception`() {
45+
val sentryId = SentryId().toString() + "abc"
46+
val spanId = SpanId()
47+
val ex =
48+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId") }
49+
assertEquals(
50+
"sentry-trace header does not conform to expected format: $sentryId-$spanId",
51+
ex.message,
52+
)
53+
}
54+
55+
@Test
56+
fun `when trace-id contains invalid characters throws exception`() {
57+
var sentryId = SentryId().toString()
58+
sentryId = sentryId.substring(0, 8) + "g" + sentryId.substring(8)
59+
val spanId = SpanId()
60+
val ex =
61+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId") }
62+
assertEquals(
63+
"sentry-trace header does not conform to expected format: $sentryId-$spanId",
64+
ex.message,
65+
)
66+
}
67+
68+
@Test
69+
fun `when span-id has less than 16 characters throws exception`() {
70+
val sentryId = SentryId()
71+
val spanId = SpanId().toString().substring(0, 8)
72+
val ex =
73+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId") }
74+
assertEquals(
75+
"sentry-trace header does not conform to expected format: $sentryId-$spanId",
76+
ex.message,
77+
)
78+
}
79+
80+
@Test
81+
fun `when span-id has more than 32 characters throws exception`() {
82+
val sentryId = SentryId()
83+
val spanId = SpanId().toString() + "abc"
84+
val ex =
85+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId") }
86+
assertEquals(
87+
"sentry-trace header does not conform to expected format: $sentryId-$spanId",
88+
ex.message,
89+
)
90+
}
91+
92+
@Test
93+
fun `when span-id contains invalid characters throws exception`() {
94+
val sentryId = SentryId()
95+
var spanId = SpanId().toString()
96+
spanId = spanId.substring(0, 8) + "g" + spanId.substring(8)
97+
val ex =
98+
assertFailsWith<InvalidSentryTraceHeaderException> { SentryTraceHeader("$sentryId-$spanId") }
99+
assertEquals(
100+
"sentry-trace header does not conform to expected format: $sentryId-$spanId",
101+
ex.message,
102+
)
103+
}
104+
18105
@Test
19106
fun `handles header with positive sampling decision`() {
20107
val sentryId = SentryId()

0 commit comments

Comments
 (0)