Skip to content

Hardening: Add configurable element count limit per collection to prevent resource exhaustion from oversized card payloads #9374

@karan68

Description

@karan68

Summary
The shared C++ object model parser has no upper bound on the number of items it will parse from any JSON array collection (body elements, actions, columns, choices, facts, etc.). A card payload with 100,000 elements in a single collection creates 100,000 parsed C++ objects in memory with zero resistance from the parser.

While this is by-design for trusted-source cards, it becomes a resource exhaustion vector when AdaptiveCards renders untrusted 3rd-party content (e.g., Windows Widgets accepting widget provider payloads).

Problem
Three collection parsing functions loop over every item in the JSON array with no cap:

GetElementCollection (ParseUtil.h:271):

const size_t elemSize = elementArray.size();
elements.reserve(elemSize);          // allocates for ALL items upfront
for (auto& curJsonValue : elementArray)
{
    // ... parse each item, push_back — no limit check
}

GetElementCollectionOfSingleType (ParseUtil.h:203):

elements.reserve(elementArray.size());
for (const Json::Value& curJsonValue : elementArray)
{
    auto el = deserializer(context, curJsonValue);
    elements.push_back(el);          // no limit
}

GetActionCollection (ParseUtil.cpp:446):

elements.reserve(elementArray.size());
for (const auto& curJsonValue : elementArray)
{
    auto action = ParseUtil::GetActionFromJsonValue(context, curJsonValue);
    elements.push_back(action);      // no limit
}

Additionally, elements.reserve(elementArray.size()) allocates memory for the full array size before any parsing begins —> even a 100K-element array triggers a large upfront allocation.

Affected Collections (19 call sites)

Caller                          | Collection key | Element type
--------------------------------|----------------|----------------------
SharedAdaptiveCard.cpp:244      | body           | Any card element
SharedAdaptiveCard.cpp:246      | actions        | Any action
Container.cpp:76                | items          | Any card element
Column.cpp:117                  | items          | Any card element
ColumnSet.cpp:49                | columns        | Column
Carousel.cpp:155                | pages          | CarouselPage
ChoiceSetInput.cpp:144          | choices        | ChoiceInput
FactSet.cpp:48                  | facts          | Fact
ImageSet.cpp:65                 | images         | Image
Table.cpp:108                   | rows           | TableRow
TableRow.cpp:124                | cells          | TableCell
RichTextBlock.cpp:69            | inlines        | Inline
Media.cpp:100                   | sources        | MediaSource
Media.cpp:127                   | captionSources | CaptionSource
ActionSet.cpp:54                | actions        | Any action
Authentication.cpp:124          | buttons        | AuthCardButton
ToggleVisibilityAction.cpp:42   | targetElements | ToggleVisibilityTarget

Verified Behavior
Tested with a standalone C++ binary linked against the ObjectModel library:


Collection             | Input count | JSON size | Parse time | Objects created
-----------------------|-------------|------------|-------------|----------------
Body (TextBlock)       | 100         | 3.8 KB     | 1ms         | 100
Body (TextBlock)       | 1,000       | 39 KB      | 9ms         | 1,000
Body (TextBlock)       | 10,000      | 399 KB     | 86ms        | 10,000
Body (TextBlock)       | 100,000     | 4.1 MB     | 853ms       | 100,000
Actions (OpenUrl)      | 100,000     | 7.5 MB     | 952ms       | 100,000
Columns                | 10,000      | 639 KB     | 326ms       | 10,000

Existing Limits

  • HostConfig::ActionsConfig::maxActions = 5 exists but is only enforced at render time in ActionHelpers.cpp:930 —> the parser still creates all action objects
  • There is no maxBodyElements, maxColumns, maxChoices, or any general-purpose collection limit
  • There is no API for a host to configure a parse-time element cap

Impact
When the parsed tree is rendered (UWP/WinUI3 XAML renderer):

  • Each parsed element becomes one or more XAML controls (TextBlock, StackPanel, Button, etc.)
  • 100,000 elements = 100,000+ XAML controls → memory exhaustion + UI thread freeze
  • In Windows Widgets: WidgetBoard.exe becomes unresponsive
  • Any 3rd-party widget provider can send such a payload

Environment

  • Component: Shared C++ ObjectModel
  • Affects: All platforms using the shared model (UWP, WinUI3, Android, iOS)
  • AdaptiveCards version: Current main branch (verified May 2026)

Note
This is a defense-in-depth hardening request, not a crash bug. The parser works as originally designed —> it faithfully deserializes everything. The gap is that there's no opt-in (or default) limit for hosts that render untrusted content. The threat model expanded when Windows Widgets began using AdaptiveCards for 3rd-party payloads.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions