Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<AdaptiveCard> card)
{
if (!card) return 0;
auto& actions = card->GetActions();
for (auto& action : actions)
{
if (action->GetElementType() == ActionType::ShowCard)
{
auto showCard = std::static_pointer_cast<ShowCardAction>(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<int>(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<int>(ParseContext::c_maxShowCardDepth));
}
};
}
18 changes: 18 additions & 0 deletions source/shared/cpp/ObjectModel/ParseContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions source/shared/cpp/ObjectModel/ParseContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ class ParseContext
void RemoveProhibitedElementType(const std::vector<std::string>& 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
Expand Down Expand Up @@ -84,6 +90,8 @@ class ParseContext

std::unordered_set<std::string> m_prohibitedElementTypes;

unsigned int m_currentShowCardDepth{0};

bool m_canFallbackToAncestor;
std::string m_language;
};
Expand Down
10 changes: 10 additions & 0 deletions source/shared/cpp/ObjectModel/ShowCardAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,17 @@ std::shared_ptr<BaseActionElement> ShowCardActionParser::Deserialize(ParseContex

const std::string& propertyName = AdaptiveCardSchemaKeyToString(AdaptiveCardSchemaKey::Card);

if (!context.CanIncrementShowCardDepth())
{
context.warnings.push_back(std::make_shared<AdaptiveCardParseWarning>(
WarningStatusCode::CustomWarning, "Maximum ShowCard nesting depth exceeded"));
showCardAction->SetCard(std::make_shared<AdaptiveCard>());
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());
Expand Down