bej_decoder_json: Add a limit on # of 0s in "real"

We have count the number of 0s in real numbers to add to the output
string. To prevent OOM from many 0s, add a limit.

Tested: Add unit test

Change-Id: I5e298554cf9d93b3f7ab86cac5b0b0a6f443716b
Signed-off-by: Brandon Kim <brandonkim@google.com>
diff --git a/test/bej_decoder_test.cpp b/test/bej_decoder_test.cpp
index 368bbc9..19cdb92 100644
--- a/test/bej_decoder_test.cpp
+++ b/test/bej_decoder_test.cpp
@@ -141,6 +141,67 @@
                 bejErrorNotSupported);
 }
 
+TEST(BejDecoderSecurityTest, RealWithTooManyLeadingZeros)
+{
+    auto inputsOrErr = loadInputs(dummySimpleTestFiles);
+    ASSERT_TRUE(inputsOrErr);
+
+    BejDictionaries dictionaries = {
+        .schemaDictionary = inputsOrErr->schemaDictionary,
+        .annotationDictionary = inputsOrErr->annotationDictionary,
+        .errorDictionary = inputsOrErr->errorDictionary,
+    };
+
+    auto root = std::make_unique<RedfishPropertyParent>();
+    bejTreeInitSet(root.get(), "DummySimple");
+
+    // 1.003 was randomely chosen
+    auto real = std::make_unique<RedfishPropertyLeafReal>();
+    bejTreeAddReal(root.get(), real.get(), "SampleRealProperty", 1.003);
+
+    libbej::BejEncoderJson encoder;
+    encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
+    std::vector<uint8_t> outputBuffer = encoder.getOutput();
+
+    // Manually tamper with the encoded stream to create the attack vector.
+    // We will find the `bejReal` property and overwrite its `zeroCount`.
+    // The property "SampleRealProperty" has sequence number 4. The encoded
+    // sequence number is `(4 << 1) | 0 = 8`. The nnint for 8 is `0x01, 0x08`.
+    const std::vector<uint8_t> realPropSeqNum = {0x01, 0x08};
+    auto it = std::search(outputBuffer.begin(), outputBuffer.end(),
+                          realPropSeqNum.begin(), realPropSeqNum.end());
+    ASSERT_NE(it, outputBuffer.end()) << "Could not find bejReal property";
+
+    // The structure of a bejReal SFLV is: S(nnint) F(u8) L(nnint) V(...)
+    // The structure of V is: nnint(len(whole)), int(whole), nnint(zeroCount)...
+    // Find the start of the value (V) by skipping S, F, and L.
+    size_t sflvOffset = std::distance(outputBuffer.begin(), it);
+    const uint8_t* streamPtr = outputBuffer.data() + sflvOffset;
+    // Skip S
+    streamPtr += bejGetNnintSize(streamPtr);
+    // Skip F
+    streamPtr++;
+    // Skip L
+    const uint8_t* valuePtr = streamPtr + bejGetNnintSize(streamPtr);
+
+    // Find the start of zeroCount within V.
+    const uint8_t* zeroCountPtr = valuePtr + bejGetNnintSize(valuePtr);
+    zeroCountPtr += bejGetNnint(valuePtr); // Skip int(whole)
+
+    // The original zeroCount for 1.003 is 2. nnint(2) is `0x01, 0x02`.
+    // We replace it with a zeroCount of 101, which exceeds the limit.
+    // nnint(101) is `0x01, 101`. The size is the same (2 bytes), so we don't
+    // need to update the SFLV length field (L).
+    ASSERT_EQ(bejGetNnint(zeroCountPtr), 2);
+    size_t zeroCountOffset = zeroCountPtr - outputBuffer.data();
+    // nnint value for 101
+    outputBuffer[zeroCountOffset + 1] = 101;
+
+    BejDecoderJson decoder;
+    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
+                bejErrorInvalidSize);
+}
+
 /**
  * TODO: Add more test cases.
  * - Test Enums inside array elemets