|
4 | 4 | #include <gtest/gtest.h> |
5 | 5 | #include <cassert> |
6 | 6 | #include <cstddef> |
| 7 | +#include <initializer_list> |
7 | 8 | #include <string> |
| 9 | +#include <utility> |
8 | 10 | #include <vector> |
9 | 11 |
|
10 | 12 | #include "opentelemetry/baggage/baggage.h" |
|
15 | 17 |
|
16 | 18 | using namespace opentelemetry::baggage; |
17 | 19 |
|
| 20 | +TEST(BaggageTest, RejectInvalidMetadataOnExtractAndInject) |
| 21 | +{ |
| 22 | + const std::vector<std::string> invalid = { |
| 23 | + ";a\r\nX-Injected: 1", |
| 24 | + ";a\r\n\r\nGET /smuggled HTTP/1.1\r\nHost: localhost\r\nX-Smuggled: 1", |
| 25 | + ";a\rb", |
| 26 | + ";a\nb", |
| 27 | + std::string(";a\0b", 4), |
| 28 | + ";a=\x7f", |
| 29 | + ";a=\xc3\xa9", |
| 30 | + ";", |
| 31 | + ";a;;b", |
| 32 | + ";=value", |
| 33 | + ";bad key=value", |
| 34 | + ";a=bad value", |
| 35 | + ";a=bad\tvalue", |
| 36 | + ";a=\"quoted\"", |
| 37 | + ";a=back\\slash"}; |
| 38 | + for (const auto &metadata : invalid) |
| 39 | + { |
| 40 | + SCOPED_TRACE(metadata); |
| 41 | + auto baggage = Baggage::FromHeader("before=ok,bad=value" + metadata + ",after=ok"); |
| 42 | + std::string value; |
| 43 | + EXPECT_FALSE(baggage->GetValue("bad", value)); |
| 44 | + EXPECT_TRUE(baggage->GetValue("before", value)); |
| 45 | + EXPECT_TRUE(baggage->GetValue("after", value)); |
| 46 | + EXPECT_EQ(baggage->ToHeader(), "before=ok,after=ok"); |
| 47 | + |
| 48 | + // Direct construction does not validate input. Output must still be safe. |
| 49 | + const std::vector<std::pair<std::string, std::string>> entries = { |
| 50 | + {"before", "ok"}, {"bad", "value" + metadata}, {"after", "ok"}}; |
| 51 | + // KeyValueProperties exposes NUL-terminated strings, so direct construction |
| 52 | + // already truncates at an embedded NUL. Extraction must reject it above. |
| 53 | + if (metadata.find('\0') == std::string::npos) |
| 54 | + { |
| 55 | + EXPECT_EQ(Baggage(entries).ToHeader(), "before=ok,after=ok"); |
| 56 | + } |
| 57 | + EXPECT_EQ(Baggage().Set("bad", "value" + metadata)->ToHeader(), ""); |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +TEST(BaggageTest, RejectMemberInjectionFromSet) |
| 62 | +{ |
| 63 | + auto baggage = Baggage::FromHeader("good=ok")->Set("k", "v;a,evil=1"); |
| 64 | + EXPECT_EQ(baggage->ToHeader(), "good=ok"); |
| 65 | +} |
| 66 | + |
| 67 | +TEST(BaggageTest, SetWithInvalidMetadataPreservesExistingEntry) |
| 68 | +{ |
| 69 | + auto original = Baggage::FromHeader("k=good;flag,other=kept"); |
| 70 | + for (const auto *invalid : {"v;a,evil=1", "v;", "v;;x", "v;p:x", "v;p=bad value"}) |
| 71 | + { |
| 72 | + SCOPED_TRACE(invalid); |
| 73 | + auto updated = original->Set("k", invalid); |
| 74 | + std::string value; |
| 75 | + ASSERT_TRUE(updated->GetValue("k", value)); |
| 76 | + EXPECT_EQ(value, "good;flag"); |
| 77 | + EXPECT_EQ(updated->ToHeader(), "k=good;flag,other=kept"); |
| 78 | + EXPECT_EQ(original->ToHeader(), "k=good;flag,other=kept"); |
| 79 | + |
| 80 | + auto inserted = original->Set("new", invalid); |
| 81 | + EXPECT_FALSE(inserted->GetValue("new", value)); |
| 82 | + EXPECT_EQ(inserted->ToHeader(), "k=good;flag,other=kept"); |
| 83 | + } |
| 84 | + |
| 85 | + // Valid metadata must still replace the existing value. |
| 86 | + auto updated = original->Set("k", "replacement;p=100%"); |
| 87 | + std::string value; |
| 88 | + ASSERT_TRUE(updated->GetValue("k", value)); |
| 89 | + EXPECT_EQ(value, "replacement;p=100%"); |
| 90 | + EXPECT_EQ(updated->ToHeader(), "k=replacement;p=100%,other=kept"); |
| 91 | +} |
| 92 | + |
| 93 | +TEST(BaggageTest, RejectMemberInjectionFromDecodedDelimiters) |
| 94 | +{ |
| 95 | + // Decoding the value introduces a semicolon that output treats as metadata. |
| 96 | + auto baggage = Baggage::FromHeader("k=a%3B%2Cx%3Dy,good=ok"); |
| 97 | + EXPECT_EQ(baggage->ToHeader(), "good=ok"); |
| 98 | +} |
| 99 | + |
| 100 | +TEST(BaggageTest, PreserveValidMetadataWireRepresentation) |
| 101 | +{ |
| 102 | + for (const std::string metadata : |
| 103 | + {";flag", ";empty=", ";a=b=c", ";p=100%", ";a=%20%0D%0A%25", "; flag \t; key \t=\t value", |
| 104 | + ";a=!#$&'()*+-./:<=>?@[]^_`{|}~"}) |
| 105 | + { |
| 106 | + SCOPED_TRACE(metadata); |
| 107 | + const auto header = "key=value" + metadata; |
| 108 | + EXPECT_EQ(Baggage::FromHeader(header)->ToHeader(), header); |
| 109 | + const std::vector<std::pair<std::string, std::string>> entries = {{"key", "value" + metadata}}; |
| 110 | + EXPECT_EQ(Baggage(entries).ToHeader(), header); |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +TEST(BaggageTest, RejectUnsafeBytesAfterTokenization) |
| 115 | +{ |
| 116 | + for (int byte = 0; byte <= 255; ++byte) |
| 117 | + { |
| 118 | + if ((byte >= 0x20 && byte <= 0x7e) || byte == '\t') |
| 119 | + { |
| 120 | + continue; |
| 121 | + } |
| 122 | + SCOPED_TRACE(byte); |
| 123 | + const std::string control(1, static_cast<char>(byte)); |
| 124 | + // Interior bytes must be checked in both the key and the complete value. |
| 125 | + for (const auto &member : |
| 126 | + {"bad" + control + "key=value;flag", "bad=value;flag=" + control + "value"}) |
| 127 | + { |
| 128 | + EXPECT_EQ(Baggage::FromHeader(member + ",good=ok")->ToHeader(), "good=ok"); |
| 129 | + } |
| 130 | + // The shared tokenizer has always normalized these whitespace bytes at edges. |
| 131 | + const bool trimmed = byte >= 0x0a && byte <= 0x0d; |
| 132 | + for (const auto &member : {control + "bad=value;flag", "bad=value;flag" + control}) |
| 133 | + { |
| 134 | + EXPECT_EQ(Baggage::FromHeader(member + ",good=ok")->ToHeader(), |
| 135 | + trimmed ? "bad=value;flag,good=ok" : "good=ok"); |
| 136 | + } |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +TEST(BaggageTest, PreserveTokenizerEdgeWhitespaceNormalization) |
| 141 | +{ |
| 142 | + EXPECT_EQ(Baggage::FromHeader(" \t\r\n\v\fk=v;flag\r\n\v\f \t,other=ok")->ToHeader(), |
| 143 | + "k=v;flag,other=ok"); |
| 144 | + // Directly constructed metadata is not tokenizer-normalized and must remain rejected. |
| 145 | + const std::vector<std::pair<std::string, std::string>> entries = {{"k", "v;flag\r\n"}}; |
| 146 | + EXPECT_EQ(Baggage(entries).ToHeader(), ""); |
| 147 | +} |
| 148 | + |
18 | 149 | static std::string header_with_custom_entries(size_t num_entries) |
19 | 150 | { |
20 | 151 | std::string header; |
@@ -127,8 +258,8 @@ TEST(BaggageTest, ValidateInjectHeader) |
127 | 258 | {"bar%1", "bar 4"}, |
128 | 259 | "a%2A%2Ffoo-_%2Fbar=bar+4,1a-2f%40foo=bar%251"}, // encoding is done properly |
129 | 260 | {{"foo 1"}, |
130 | | - {"bar 1; metadata ; ;;"}, |
131 | | - "foo+1=bar+1; metadata ; ;;"}}; // metadata is added without encoding |
| 261 | + {"bar 1; metadata ; flag=encoded%20value"}, |
| 262 | + "foo+1=bar+1; metadata ; flag=encoded%20value"}}; |
132 | 263 |
|
133 | 264 | for (auto &testcase : testcases) |
134 | 265 | { |
|
0 commit comments