microsoft / microsoft/AdaptiveCards

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

Open
#9,374 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
2k
Forks
595
Avg merge
1d 19h
Merged PRs (30d)
1

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ParseUtil.h at GetElementCollection and GetElementCollectionOfSingleType, then inspect ParseUtil.cpp at GetActionCollection and the existing render-time limit in ActionHelpers.cpp:930. Define how hosts configure a parse-time cap and apply it across the listed collection call sites, including the upfront reserve behavior. Done means oversized collections no longer create unbounded parsed objects and the limit is configurable.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.