blob: cf76d2b8eb9aab608cbfe54bd9d43b7de7c78bbc [file] [log] [blame]
Nikhil Namjoshida6e5572023-03-13 10:52:53 -07001#include "bej_common_test.hpp"
kasunathe8946af2022-05-23 12:32:09 -07002#include "bej_decoder_json.hpp"
Brandon Kim07abbf82025-06-23 21:03:07 +00003#include "bej_encoder_json.hpp"
kasunathe8946af2022-05-23 12:32:09 -07004
kasunathe8946af2022-05-23 12:32:09 -07005#include <memory>
kasunathe8946af2022-05-23 12:32:09 -07006#include <string_view>
Brandon Kim07abbf82025-06-23 21:03:07 +00007#include <vector>
kasunathe8946af2022-05-23 12:32:09 -07008
9#include <gmock/gmock-matchers.h>
10#include <gmock/gmock.h>
11#include <gtest/gtest.h>
12
13namespace libbej
14{
15
kasunathe8946af2022-05-23 12:32:09 -070016struct BejDecoderTestParams
17{
18 const std::string testName;
19 const BejTestInputFiles inputFiles;
20};
21
Brian Maa46f9852024-09-27 16:57:20 +080022void PrintTo(const BejDecoderTestParams& params, std::ostream* os)
23{
24 *os << params.testName;
25}
26
kasunathe8946af2022-05-23 12:32:09 -070027using BejDecoderTest = testing::TestWithParam<BejDecoderTestParams>;
28
29const 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
37const 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
45const 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
53const 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
kasunathe8946af2022-05-23 12:32:09 -070061TEST_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 .annotationDictionary = inputsOrErr->annotationDictionary,
70 .errorDictionary = inputsOrErr->errorDictionary,
71 };
72
73 BejDecoderJson decoder;
74 EXPECT_THAT(decoder.decode(dictionaries, inputsOrErr->encodedStream), 0);
75 std::string decoded = decoder.getOutput();
76 nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
77
78 // Just comparing nlohmann::json types could lead to errors. It compares the
79 // byte values. So int64 and unit64 comparisons might be incorrect. Eg:
80 // bytes values for -5 and 18446744073709551611 are the same. So compare the
81 // string values.
82 EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
83}
84
Kasun Athukorala6fd03fb2025-07-15 05:19:39 +000085/**
86 * TODO: Add more test cases.
87 * - Test Enums inside array elemets
88 * - Array inside an array: is this a valid case?
89 * - Real numbers with exponent part
90 * - Every type inside an array.
91 */
92INSTANTIATE_TEST_SUITE_P(
93 , BejDecoderTest,
94 testing::ValuesIn<BejDecoderTestParams>({
95 {"DriveOEM", driveOemTestFiles},
96 {"Circuit", circuitTestFiles},
97 {"Storage", storageTestFiles},
98 {"DummySimple", dummySimpleTestFiles},
99 }),
100 [](const testing::TestParamInfo<BejDecoderTest::ParamType>& info) {
101 return info.param.testName;
102 });
103
Brandon Kim07abbf82025-06-23 21:03:07 +0000104TEST(BejDecoderSecurityTest, MaxOperationsLimit)
105{
106 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
107 ASSERT_TRUE(inputsOrErr);
108
109 BejDictionaries dictionaries = {
110 .schemaDictionary = inputsOrErr->schemaDictionary,
111 .annotationDictionary = inputsOrErr->annotationDictionary,
112 .errorDictionary = inputsOrErr->errorDictionary,
113 };
114
115 // Each array element below consists of a set and two properties, resulting
116 // in 3 operations. 400,000 elements will result in 1,200,000 operations,
117 // which should exceed the limit of 1,000,000.
118 constexpr int numElements = 400000;
119
120 auto root = std::make_unique<RedfishPropertyParent>();
121 bejTreeInitSet(root.get(), "DummySimple");
122
123 auto childArray = std::make_unique<RedfishPropertyParent>();
124 bejTreeInitArray(childArray.get(), "ChildArrayProperty");
125 bejTreeLinkChildToParent(root.get(), childArray.get());
126
127 std::vector<std::unique_ptr<RedfishPropertyParent>> sets;
128 std::vector<std::unique_ptr<RedfishPropertyLeafBool>> bools;
129 std::vector<std::unique_ptr<RedfishPropertyLeafEnum>> enums;
130 sets.reserve(numElements);
131 bools.reserve(numElements);
132 enums.reserve(numElements);
133
134 for (int i = 0; i < numElements; ++i)
135 {
136 auto chArraySet = std::make_unique<RedfishPropertyParent>();
137 bejTreeInitSet(chArraySet.get(), nullptr);
138
139 auto chArraySetBool = std::make_unique<RedfishPropertyLeafBool>();
140 bejTreeAddBool(chArraySet.get(), chArraySetBool.get(), "AnotherBoolean",
141 true);
142
143 auto chArraySetLs = std::make_unique<RedfishPropertyLeafEnum>();
144 bejTreeAddEnum(chArraySet.get(), chArraySetLs.get(), "LinkStatus",
145 "NoLink");
146
147 bejTreeLinkChildToParent(childArray.get(), chArraySet.get());
148
149 sets.push_back(std::move(chArraySet));
150 bools.push_back(std::move(chArraySetBool));
151 enums.push_back(std::move(chArraySetLs));
152 }
153
154 libbej::BejEncoderJson encoder;
155 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
156 std::vector<uint8_t> outputBuffer = encoder.getOutput();
157
158 BejDecoderJson decoder;
159 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
Brandon Kimef01d4c2025-06-23 21:13:04 +0000160 bejErrorNotSupported);
Brandon Kim07abbf82025-06-23 21:03:07 +0000161}
162
Brandon Kim9eb01172025-06-23 21:26:59 +0000163TEST(BejDecoderSecurityTest, RealWithTooManyLeadingZeros)
164{
165 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
166 ASSERT_TRUE(inputsOrErr);
167
168 BejDictionaries dictionaries = {
169 .schemaDictionary = inputsOrErr->schemaDictionary,
170 .annotationDictionary = inputsOrErr->annotationDictionary,
171 .errorDictionary = inputsOrErr->errorDictionary,
172 };
173
174 auto root = std::make_unique<RedfishPropertyParent>();
175 bejTreeInitSet(root.get(), "DummySimple");
176
177 // 1.003 was randomely chosen
178 auto real = std::make_unique<RedfishPropertyLeafReal>();
179 bejTreeAddReal(root.get(), real.get(), "SampleRealProperty", 1.003);
180
181 libbej::BejEncoderJson encoder;
182 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
183 std::vector<uint8_t> outputBuffer = encoder.getOutput();
184
185 // Manually tamper with the encoded stream to create the attack vector.
186 // We will find the `bejReal` property and overwrite its `zeroCount`.
187 // The property "SampleRealProperty" has sequence number 4. The encoded
188 // sequence number is `(4 << 1) | 0 = 8`. The nnint for 8 is `0x01, 0x08`.
189 const std::vector<uint8_t> realPropSeqNum = {0x01, 0x08};
190 auto it = std::search(outputBuffer.begin(), outputBuffer.end(),
191 realPropSeqNum.begin(), realPropSeqNum.end());
192 ASSERT_NE(it, outputBuffer.end()) << "Could not find bejReal property";
193
194 // The structure of a bejReal SFLV is: S(nnint) F(u8) L(nnint) V(...)
195 // The structure of V is: nnint(len(whole)), int(whole), nnint(zeroCount)...
196 // Find the start of the value (V) by skipping S, F, and L.
197 size_t sflvOffset = std::distance(outputBuffer.begin(), it);
198 const uint8_t* streamPtr = outputBuffer.data() + sflvOffset;
199 // Skip S
200 streamPtr += bejGetNnintSize(streamPtr);
201 // Skip F
202 streamPtr++;
203 // Skip L
204 const uint8_t* valuePtr = streamPtr + bejGetNnintSize(streamPtr);
205
206 // Find the start of zeroCount within V.
207 const uint8_t* zeroCountPtr = valuePtr + bejGetNnintSize(valuePtr);
208 zeroCountPtr += bejGetNnint(valuePtr); // Skip int(whole)
209
210 // The original zeroCount for 1.003 is 2. nnint(2) is `0x01, 0x02`.
211 // We replace it with a zeroCount of 101, which exceeds the limit.
212 // nnint(101) is `0x01, 101`. The size is the same (2 bytes), so we don't
213 // need to update the SFLV length field (L).
214 ASSERT_EQ(bejGetNnint(zeroCountPtr), 2);
215 size_t zeroCountOffset = zeroCountPtr - outputBuffer.data();
216 // nnint value for 101
217 outputBuffer[zeroCountOffset + 1] = 101;
218
219 BejDecoderJson decoder;
220 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
221 bejErrorInvalidSize);
222}
223
Kasun Athukorala485044b2025-07-15 00:59:36 +0000224TEST(BejDecoderSecurityTest, StringTooLong)
225{
226 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
227 ASSERT_TRUE(inputsOrErr);
228
229 BejDictionaries dictionaries = {
230 .schemaDictionary = inputsOrErr->schemaDictionary,
231 .annotationDictionary = inputsOrErr->annotationDictionary,
232 .errorDictionary = inputsOrErr->errorDictionary,
233 };
234
235 auto root = std::make_unique<RedfishPropertyParent>();
236 bejTreeInitSet(root.get(), "DummySimple");
237
238 // Create a string with a length greater than MAX_BEJ_STRING_LEN (65536).
239 std::string longString(65537, 'A');
240
241 auto stringProp = std::make_unique<RedfishPropertyLeafString>();
242 bejTreeAddString(root.get(), stringProp.get(), "Id", longString.c_str());
243
244 libbej::BejEncoderJson encoder;
245 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
246 std::vector<uint8_t> outputBuffer = encoder.getOutput();
247
248 // The decoder should return an error because the string is too long.
249 BejDecoderJson decoder;
250 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
251 bejErrorInvalidSize);
252}
253
Kasun Athukorala6fd03fb2025-07-15 05:19:39 +0000254TEST(BejDecoderSecurityTest, ValueBeyondStreamLength)
255{
256 auto inputsOrErr = loadInputs(dummySimpleTestFiles);
257 ASSERT_TRUE(inputsOrErr);
258
259 BejDictionaries dictionaries = {
260 .schemaDictionary = inputsOrErr->schemaDictionary,
261 .annotationDictionary = inputsOrErr->annotationDictionary,
262 .errorDictionary = inputsOrErr->errorDictionary,
263 };
264
265 auto root = std::make_unique<RedfishPropertyParent>();
266 bejTreeInitSet(root.get(), "DummySimple");
267
268 auto intProp = std::make_unique<RedfishPropertyLeafInt>();
269 bejTreeAddInteger(root.get(), intProp.get(), "SampleIntegerProperty", 123);
270
271 libbej::BejEncoderJson encoder;
272 encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
273 std::vector<uint8_t> outputBuffer = encoder.getOutput();
274
275 // Tamper with the encoded stream to simulate a value extending beyond the
276 // stream length. This stream only has an integer 0x7b. The value before it
277 // is the length tuple.
278 outputBuffer[outputBuffer.size() - 2] = 0x05;
279
280 BejDecoderJson decoder;
281 EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
282 bejErrorInvalidSize);
283}
kasunathe8946af2022-05-23 12:32:09 -0700284
285} // namespace libbej