1 #include "bej_common_test.hpp"
2 #include "bej_decoder_json.hpp"
3 #include "bej_encoder_json.hpp"
4
5 #include <memory>
6 #include <string_view>
7 #include <vector>
8
9 #include <gmock/gmock-matchers.h>
10 #include <gmock/gmock.h>
11 #include <gtest/gtest.h>
12
13 namespace libbej
14 {
15
16 struct BejDecoderTestParams
17 {
18 const std::string testName;
19 const BejTestInputFiles inputFiles;
20 };
21
PrintTo(const BejDecoderTestParams & params,std::ostream * os)22 void PrintTo(const BejDecoderTestParams& params, std::ostream* os)
23 {
24 *os << params.testName;
25 }
26
27 using BejDecoderTest = testing::TestWithParam<BejDecoderTestParams>;
28
29 const BejTestInputFiles driveOemTestFiles = {
30 .jsonFile = "../test/json/drive_oem.json",
31 .schemaDictionaryFile = "../test/dictionaries/drive_oem_dict.bin",
32 .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
33 .errorDictionaryFile = "",
34 .encodedStreamFile = "../test/encoded/drive_oem_enc.bin",
35 };
36
37 const BejTestInputFiles circuitTestFiles = {
38 .jsonFile = "../test/json/circuit.json",
39 .schemaDictionaryFile = "../test/dictionaries/circuit_dict.bin",
40 .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
41 .errorDictionaryFile = "",
42 .encodedStreamFile = "../test/encoded/circuit_enc.bin",
43 };
44
45 const BejTestInputFiles storageTestFiles = {
46 .jsonFile = "../test/json/storage.json",
47 .schemaDictionaryFile = "../test/dictionaries/storage_dict.bin",
48 .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
49 .errorDictionaryFile = "",
50 .encodedStreamFile = "../test/encoded/storage_enc.bin",
51 };
52
53 const BejTestInputFiles dummySimpleTestFiles = {
54 .jsonFile = "../test/json/dummysimple.json",
55 .schemaDictionaryFile = "../test/dictionaries/dummy_simple_dict.bin",
56 .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
57 .errorDictionaryFile = "",
58 .encodedStreamFile = "../test/encoded/dummy_simple_enc.bin",
59 };
60
TEST_P(BejDecoderTest,Decode)61 TEST_P(BejDecoderTest, Decode)
62 {
63 const BejDecoderTestParams& test_case = GetParam();
64 auto inputsOrErr = loadInputs(test_case.inputFiles);
65 EXPECT_TRUE(inputsOrErr);
66
67 BejDictionaries dictionaries = {
68 .schemaDictionary = inputsOrErr->schemaDictionary,
69 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
70 .annotationDictionary = inputsOrErr->annotationDictionary,
71 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
72 .errorDictionary = inputsOrErr->errorDictionary,
73 .errorDictionarySize = inputsOrErr->errorDictionarySize,
74 };
75
76 BejDecoderJson decoder;
77 EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
78 std::string decoded = decoder.getOutput();
79 nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
80
81 // Just comparing nlohmann::json types could lead to errors. It compares the
82 // byte values. So int64 and unit64 comparisons might be incorrect. Eg:
83 // bytes values for -5 and 18446744073709551611 are the same. So compare the
84 // string values.
85 EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
86 }
87
88 /**
89 * TODO: Add more test cases.
90 * - Test Enums inside array elemets
91 * - Array inside an array: is this a valid case?
92 * - Real numbers with exponent part
93 * - Every type inside an array.
94 */
95 INSTANTIATE_TEST_SUITE_P(
96 , BejDecoderTest,
97 testing::ValuesIn<BejDecoderTestParams>({
98 {"DriveOEM", driveOemTestFiles},
99 {"Circuit", circuitTestFiles},
100 {"Storage", storageTestFiles},
101 {"DummySimple", dummySimpleTestFiles},
102 }),
__anon72dd28ce0102(const testing::TestParamInfo<BejDecoderTest::ParamType>& info) 103 [](const testing::TestParamInfo<BejDecoderTest::ParamType>& info) {
104 return info.param.testName;
105 });
106
TEST(BejDecoderSecurityTest,MaxOperationsLimit)107 TEST(BejDecoderSecurityTest, MaxOperationsLimit)
108 {
109 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
110 ASSERT_TRUE(inputsOrErr);
111
112 BejDictionaries dictionaries = {
113 .schemaDictionary = inputsOrErr->schemaDictionary,
114 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
115 .annotationDictionary = inputsOrErr->annotationDictionary,
116 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
117 .errorDictionary = inputsOrErr->errorDictionary,
118 .errorDictionarySize = inputsOrErr->errorDictionarySize,
119 };
120
121 // Each array element below consists of a set and two properties, resulting
122 // in 3 operations. 400,000 elements will result in 1,200,000 operations,
123 // which should exceed the limit of 1,000,000.
124 constexpr int numElements = 400000;
125
126 auto root = std::make_unique<RedfishPropertyParent>();
127 bejTreeInitSet(root.get(), "DummySimple");
128
129 auto childArray = std::make_unique<RedfishPropertyParent>();
130 bejTreeInitArray(childArray.get(), "ChildArrayProperty");
131 bejTreeLinkChildToParent(root.get(), childArray.get());
132
133 std::vector<std::unique_ptr<RedfishPropertyParent>> sets;
134 std::vector<std::unique_ptr<RedfishPropertyLeafBool>> bools;
135 std::vector<std::unique_ptr<RedfishPropertyLeafEnum>> enums;
136 sets.reserve(numElements);
137 bools.reserve(numElements);
138 enums.reserve(numElements);
139
140 for (int i = 0; i < numElements; ++i)
141 {
142 auto chArraySet = std::make_unique<RedfishPropertyParent>();
143 bejTreeInitSet(chArraySet.get(), nullptr);
144
145 auto chArraySetBool = std::make_unique<RedfishPropertyLeafBool>();
146 bejTreeAddBool(chArraySet.get(), chArraySetBool.get(), "AnotherBoolean",
147 true);
148
149 auto chArraySetLs = std::make_unique<RedfishPropertyLeafEnum>();
150 bejTreeAddEnum(chArraySet.get(), chArraySetLs.get(), "LinkStatus",
151 "NoLink");
152
153 bejTreeLinkChildToParent(childArray.get(), chArraySet.get());
154
155 sets.push_back(std::move(chArraySet));
156 bools.push_back(std::move(chArraySetBool));
157 enums.push_back(std::move(chArraySetLs));
158 }
159
160 libbej::BejEncoderJson encoder;
161 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
162 std::vector<uint8_t> outputBuffer = encoder.getOutput();
163
164 BejDecoderJson decoder;
165 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
166 bejErrorNotSupported);
167 }
168
TEST(BejDecoderSecurityTest,RealWithTooManyLeadingZeros)169 TEST(BejDecoderSecurityTest, RealWithTooManyLeadingZeros)
170 {
171 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
172 ASSERT_TRUE(inputsOrErr);
173
174 BejDictionaries dictionaries = {
175 .schemaDictionary = inputsOrErr->schemaDictionary,
176 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
177 .annotationDictionary = inputsOrErr->annotationDictionary,
178 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
179 .errorDictionary = inputsOrErr->errorDictionary,
180 .errorDictionarySize = inputsOrErr->errorDictionarySize,
181 };
182
183 auto root = std::make_unique<RedfishPropertyParent>();
184 bejTreeInitSet(root.get(), "DummySimple");
185
186 // 1.003 was randomely chosen
187 auto real = std::make_unique<RedfishPropertyLeafReal>();
188 bejTreeAddReal(root.get(), real.get(), "SampleRealProperty", 1.003);
189
190 libbej::BejEncoderJson encoder;
191 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
192 std::vector<uint8_t> outputBuffer = encoder.getOutput();
193
194 // Manually tamper with the encoded stream to create the attack vector.
195 // We will find the `bejReal` property and overwrite its `zeroCount`.
196 // The property "SampleRealProperty" has sequence number 4. The encoded
197 // sequence number is `(4 << 1) | 0 = 8`. The nnint for 8 is `0x01, 0x08`.
198 const std::vector<uint8_t> realPropSeqNum = {0x01, 0x08};
199 auto it = std::search(outputBuffer.begin(), outputBuffer.end(),
200 realPropSeqNum.begin(), realPropSeqNum.end());
201 ASSERT_NE(it, outputBuffer.end()) << "Could not find bejReal property";
202
203 // The structure of a bejReal SFLV is: S(nnint) F(u8) L(nnint) V(...)
204 // The structure of V is: nnint(len(whole)), int(whole), nnint(zeroCount)...
205 // Find the start of the value (V) by skipping S, F, and L.
206 size_t sflvOffset = std::distance(outputBuffer.begin(), it);
207 const uint8_t* streamPtr = outputBuffer.data() + sflvOffset;
208 // Skip S
209 streamPtr += bejGetNnintSize(streamPtr);
210 // Skip F
211 streamPtr++;
212 // Skip L
213 const uint8_t* valuePtr = streamPtr + bejGetNnintSize(streamPtr);
214
215 // Find the start of zeroCount within V.
216 const uint8_t* zeroCountPtr = valuePtr + bejGetNnintSize(valuePtr);
217 zeroCountPtr += bejGetNnint(valuePtr); // Skip int(whole)
218
219 // The original zeroCount for 1.003 is 2. nnint(2) is `0x01, 0x02`.
220 // We replace it with a zeroCount of 101, which exceeds the limit.
221 // nnint(101) is `0x01, 101`. The size is the same (2 bytes), so we don't
222 // need to update the SFLV length field (L).
223 ASSERT_EQ(bejGetNnint(zeroCountPtr), 2);
224 size_t zeroCountOffset = zeroCountPtr - outputBuffer.data();
225 // nnint value for 101
226 outputBuffer[zeroCountOffset + 1] = 101;
227
228 BejDecoderJson decoder;
229 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
230 bejErrorInvalidSize);
231 }
232
TEST(BejDecoderSecurityTest,StringTooLong)233 TEST(BejDecoderSecurityTest, StringTooLong)
234 {
235 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
236 ASSERT_TRUE(inputsOrErr);
237
238 BejDictionaries dictionaries = {
239 .schemaDictionary = inputsOrErr->schemaDictionary,
240 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
241 .annotationDictionary = inputsOrErr->annotationDictionary,
242 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
243 .errorDictionary = inputsOrErr->errorDictionary,
244 .errorDictionarySize = inputsOrErr->errorDictionarySize,
245 };
246
247 auto root = std::make_unique<RedfishPropertyParent>();
248 bejTreeInitSet(root.get(), "DummySimple");
249
250 // Create a string with a length greater than MAX_BEJ_STRING_LEN (65536).
251 std::string longString(65537, 'A');
252
253 auto stringProp = std::make_unique<RedfishPropertyLeafString>();
254 bejTreeAddString(root.get(), stringProp.get(), "Id", longString.c_str());
255
256 libbej::BejEncoderJson encoder;
257 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
258 std::vector<uint8_t> outputBuffer = encoder.getOutput();
259
260 // The decoder should return an error because the string is too long.
261 BejDecoderJson decoder;
262 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
263 bejErrorInvalidSize);
264 }
265
TEST(BejDecoderSecurityTest,ValueBeyondStreamLength)266 TEST(BejDecoderSecurityTest, ValueBeyondStreamLength)
267 {
268 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
269 ASSERT_TRUE(inputsOrErr);
270
271 BejDictionaries dictionaries = {
272 .schemaDictionary = inputsOrErr->schemaDictionary,
273 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
274 .annotationDictionary = inputsOrErr->annotationDictionary,
275 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
276 .errorDictionary = inputsOrErr->errorDictionary,
277 .errorDictionarySize = inputsOrErr->errorDictionarySize,
278 };
279
280 auto root = std::make_unique<RedfishPropertyParent>();
281 bejTreeInitSet(root.get(), "DummySimple");
282
283 auto intProp = std::make_unique<RedfishPropertyLeafInt>();
284 bejTreeAddInteger(root.get(), intProp.get(), "SampleIntegerProperty", 123);
285
286 libbej::BejEncoderJson encoder;
287 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
288 std::vector<uint8_t> outputBuffer = encoder.getOutput();
289
290 // Tamper with the encoded stream to simulate a value extending beyond the
291 // stream length. This stream only has an integer 0x7b. The value before it
292 // is the length tuple.
293 outputBuffer[outputBuffer.size() - 2] = 0x05;
294
295 BejDecoderJson decoder;
296 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
297 bejErrorInvalidSize);
298 }
299
TEST(BejDecoderSecurityTest,InvalidSchemaDictionarySize)300 TEST(BejDecoderSecurityTest, InvalidSchemaDictionarySize)
301 {
302 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
303 ASSERT_TRUE(inputsOrErr);
304
305 BejDictionaries dictionaries = {
306 .schemaDictionary = inputsOrErr->schemaDictionary,
307 .schemaDictionarySize = 10,
308 .annotationDictionary = inputsOrErr->annotationDictionary,
309 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
310 .errorDictionary = inputsOrErr->errorDictionary,
311 .errorDictionarySize = inputsOrErr->errorDictionarySize,
312 };
313
314 BejDecoderJson decoder;
315 EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
316 bejErrorInvalidSize);
317 }
318
TEST(BejDecoderSecurityTest,InvalidAnnotationDictionarySize)319 TEST(BejDecoderSecurityTest, InvalidAnnotationDictionarySize)
320 {
321 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
322 ASSERT_TRUE(inputsOrErr);
323
324 BejDictionaries dictionaries = {
325 .schemaDictionary = inputsOrErr->schemaDictionary,
326 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
327 .annotationDictionary = inputsOrErr->annotationDictionary,
328 .annotationDictionarySize = 10,
329 .errorDictionary = inputsOrErr->errorDictionary,
330 .errorDictionarySize = inputsOrErr->errorDictionarySize,
331 };
332
333 BejDecoderJson decoder;
334 EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
335 bejErrorInvalidSize);
336 }
337
TEST(BejDecoderSecurityTest,InvalidErrorDictionarySize)338 TEST(BejDecoderSecurityTest, InvalidErrorDictionarySize)
339 {
340 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
341 ASSERT_TRUE(inputsOrErr);
342
343 BejDictionaries dictionaries = {
344 .schemaDictionary = inputsOrErr->schemaDictionary,
345 .schemaDictionarySize = inputsOrErr->schemaDictionarySize,
346 .annotationDictionary = inputsOrErr->annotationDictionary,
347 .annotationDictionarySize = inputsOrErr->annotationDictionarySize,
348 .errorDictionary = inputsOrErr->errorDictionary,
349 .errorDictionarySize = 10,
350 };
351
352 BejDecoderJson decoder;
353 EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream),
354 bejErrorInvalidSize);
355 }
356
357 } // namespace libbej
358