From 7c7e9c5ffb71bc5e19780393c7ab209e1f2fc94f Mon Sep 17 00:00:00 2001 From: Karan Yadav Date: Fri, 8 May 2026 19:21:45 +0530 Subject: [PATCH] fix: add ShowCard nesting depth limit to prevent stack overflow ShowCardActionParser::Deserialize calls AdaptiveCard::Deserialize recursively with no depth guard, allowing a malicious card with deeply nested ShowCards to cause a stack overflow crash in the XAML renderer (BuildXamlTreeFromAdaptiveCard -> BuildActions -> BuildInlineShowCard -> BuildShowCard -> recurse). This adds a depth counter to ParseContext that caps ShowCard nesting at 5 levels. ShowCards beyond the limit get an empty inner card and a parse warning is emitted. Verified: depth 200 previously created a fully-recursive tree of 200 AdaptiveCard objects; now capped at 5 with warning. Security: Fixes unbounded recursion that can crash WidgetBoard.exe Related: #9343 --- .../ObjectModelTest.cpp | 110 ++++++++++++++++++ .../shared/cpp/ObjectModel/ParseContext.cpp | 18 +++ source/shared/cpp/ObjectModel/ParseContext.h | 8 ++ .../shared/cpp/ObjectModel/ShowCardAction.cpp | 10 ++ 4 files changed, 146 insertions(+) diff --git a/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/ObjectModelTest.cpp b/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/ObjectModelTest.cpp index d79ed1379b..1b1b25c643 100644 --- a/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/ObjectModelTest.cpp +++ b/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/ObjectModelTest.cpp @@ -739,5 +739,115 @@ namespace AdaptiveCardsSharedModelUnitTest const auto serializedCard = card->SerializeToJsonValue(); Assert::IsTrue(serializedCard["body"][0]["isMultiline"].asBool()); } + + // Helper: Generate nested ShowCard JSON to the specified depth + static std::string MakeNestedShowCardJson(int depth) + { + std::string innermost = R"({"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"bottom"}]})"; + std::string current = innermost; + for (int i = 0; i < depth; i++) + { + current = R"({"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Level )" + + std::to_string(depth - i) + + R"("}],"actions":[{"type":"Action.ShowCard","title":"More","card":)" + + current + R"(}]})"; + } + return current; + } + + // Helper: Walk the parsed ShowCard tree and count actual nesting depth + static int MeasureParsedDepth(std::shared_ptr card) + { + if (!card) return 0; + auto& actions = card->GetActions(); + for (auto& action : actions) + { + if (action->GetElementType() == ActionType::ShowCard) + { + auto showCard = std::static_pointer_cast(action); + auto inner = showCard->GetCard(); + if (inner && (!inner->GetBody().empty() || !inner->GetActions().empty())) + { + return 1 + MeasureParsedDepth(inner); + } + } + } + return 0; + } + + TEST_METHOD(ShowCardNesting_WithinLimit_ParsesFully) + { + // Depth 3 is well within the limit of 5 + const auto json = MakeNestedShowCardJson(3); + const auto parseResult = AdaptiveCard::DeserializeFromString(json, "1.5"); + const auto card = parseResult->GetAdaptiveCard(); + + Assert::AreEqual(3, MeasureParsedDepth(card)); + + // No warnings about depth + const auto warnings = parseResult->GetWarnings(); + for (const auto& w : warnings) + { + Assert::AreNotEqual("Maximum ShowCard nesting depth exceeded"s, w->GetReason()); + } + } + + TEST_METHOD(ShowCardNesting_AtExactLimit_ParsesFully) + { + // Depth 5 is exactly the limit — should parse fully + const auto json = MakeNestedShowCardJson(5); + const auto parseResult = AdaptiveCard::DeserializeFromString(json, "1.5"); + const auto card = parseResult->GetAdaptiveCard(); + + Assert::AreEqual(5, MeasureParsedDepth(card)); + + // No warnings about depth + const auto warnings = parseResult->GetWarnings(); + for (const auto& w : warnings) + { + Assert::AreNotEqual("Maximum ShowCard nesting depth exceeded"s, w->GetReason()); + } + } + + TEST_METHOD(ShowCardNesting_ExceedsLimit_CappedWithWarning) + { + // Depth 10 exceeds the limit of 5 — should be capped + const auto json = MakeNestedShowCardJson(10); + const auto parseResult = AdaptiveCard::DeserializeFromString(json, "1.5"); + const auto card = parseResult->GetAdaptiveCard(); + + // Should be capped at the max depth, not 10 + const int parsedDepth = MeasureParsedDepth(card); + Assert::IsTrue(parsedDepth <= static_cast(ParseContext::c_maxShowCardDepth)); + + // Should have emitted a warning + const auto warnings = parseResult->GetWarnings(); + bool foundWarning = false; + for (const auto& w : warnings) + { + if (w->GetReason() == "Maximum ShowCard nesting depth exceeded") + { + foundWarning = true; + break; + } + } + Assert::IsTrue(foundWarning, L"Expected warning about ShowCard nesting depth"); + } + + TEST_METHOD(ShowCardNesting_DeepNesting_NoException) + { + // Depth 200 previously parsed successfully and could crash the renderer. + // Now it should be capped without throwing an exception. + const auto json = MakeNestedShowCardJson(200); + const auto parseResult = AdaptiveCard::DeserializeFromString(json, "1.5"); + const auto card = parseResult->GetAdaptiveCard(); + + // Must not crash or throw — card should be valid + Assert::IsNotNull(card.get()); + + // Depth must be capped + const int parsedDepth = MeasureParsedDepth(card); + Assert::IsTrue(parsedDepth <= static_cast(ParseContext::c_maxShowCardDepth)); + } }; } diff --git a/source/shared/cpp/ObjectModel/ParseContext.cpp b/source/shared/cpp/ObjectModel/ParseContext.cpp index ad5cbaa4b0..e413ef0a47 100644 --- a/source/shared/cpp/ObjectModel/ParseContext.cpp +++ b/source/shared/cpp/ObjectModel/ParseContext.cpp @@ -361,4 +361,22 @@ const std::string& ParseContext::GetLanguage() const { return m_language; } + +bool ParseContext::CanIncrementShowCardDepth() const +{ + return m_currentShowCardDepth < c_maxShowCardDepth; +} + +void ParseContext::IncrementShowCardDepth() +{ + m_currentShowCardDepth++; +} + +void ParseContext::DecrementShowCardDepth() +{ + if (m_currentShowCardDepth > 0) + { + m_currentShowCardDepth--; + } +} } // namespace AdaptiveCards diff --git a/source/shared/cpp/ObjectModel/ParseContext.h b/source/shared/cpp/ObjectModel/ParseContext.h index c734a52103..831824e4b4 100644 --- a/source/shared/cpp/ObjectModel/ParseContext.h +++ b/source/shared/cpp/ObjectModel/ParseContext.h @@ -52,6 +52,12 @@ class ParseContext void RemoveProhibitedElementType(const std::vector& list); void ShouldParse(const std::string& type); + // ShowCard nesting depth tracking — prevents stack overflow from deeply nested ShowCards + static constexpr unsigned int c_maxShowCardDepth = 5; + bool CanIncrementShowCardDepth() const; + void IncrementShowCardDepth(); + void DecrementShowCardDepth(); + private: const AdaptiveCards::InternalId GetNearestFallbackId(const AdaptiveCards::InternalId& skipId) const; // This enum is just a helper to keep track of the position of contents within the std::tuple used in @@ -84,6 +90,8 @@ class ParseContext std::unordered_set m_prohibitedElementTypes; + unsigned int m_currentShowCardDepth{0}; + bool m_canFallbackToAncestor; std::string m_language; }; diff --git a/source/shared/cpp/ObjectModel/ShowCardAction.cpp b/source/shared/cpp/ObjectModel/ShowCardAction.cpp index d16c86f3da..18369e5318 100644 --- a/source/shared/cpp/ObjectModel/ShowCardAction.cpp +++ b/source/shared/cpp/ObjectModel/ShowCardAction.cpp @@ -47,7 +47,17 @@ std::shared_ptr ShowCardActionParser::Deserialize(ParseContex const std::string& propertyName = AdaptiveCardSchemaKeyToString(AdaptiveCardSchemaKey::Card); + if (!context.CanIncrementShowCardDepth()) + { + context.warnings.push_back(std::make_shared( + WarningStatusCode::CustomWarning, "Maximum ShowCard nesting depth exceeded")); + showCardAction->SetCard(std::make_shared()); + return showCardAction; + } + + context.IncrementShowCardDepth(); auto parseResult = AdaptiveCard::Deserialize(json.get(propertyName, Json::Value()), "", context); + context.DecrementShowCardDepth(); auto showCardWarnings = parseResult->GetWarnings(); auto warningsEnd = context.warnings.insert(context.warnings.end(), showCardWarnings.begin(), showCardWarnings.end());