Skip to content

fix(security): prevent memory exhaustion in inline image decoding#22325

Merged
BradGroux merged 1 commit intoopenclaw:mainfrom
hackersifu:main
Apr 1, 2026
Merged

fix(security): prevent memory exhaustion in inline image decoding#22325
BradGroux merged 1 commit intoopenclaw:mainfrom
hackersifu:main

Conversation

@hackersifu
Copy link
Copy Markdown
Contributor

@hackersifu hackersifu commented Feb 21, 2026

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: MS Teams inbound attachment parsing is vulnerable to memory exhaustion (DoS). Inline data:image/...;base64,... content from HTML attachments is decoded directly into a Buffer before size limits are enforced. An attacker can send a large base64 payload that triggers massive allocations during Buffer.from() decode, causing high memory pressure or process instability.

  • Why it matters: This is a denial-of-service vector. An attacker can send specially crafted Teams messages with oversized inline images to exhaust process memory, crash the service, or degrade performance for legitimate users.

  • What changed:

    • Added estimateBase64DecodedBytes() and isLikelyBase64Payload() helpers to validate and estimate decoded size before allocation
    • Renamed decodeDataImage()decodeDataImageWithLimits() to enforce per-image byte limits before Buffer.from()
    • Added InlineImageLimitOptions type for per-image and cumulative byte budgets
    • Updated extractInlineImageCandidates() to accept and enforce limits, tracking total estimated bytes
    • Connected limits in downloadMSTeamsAttachments() to pass byte constraints through the call path
  • What did NOT change: User-facing API behavior remains identical; limits enforcement is internal to security-sensitive parsing paths.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #[vulnerability-tracking-issue]
  • Related: MS Teams attachment security review

User-visible / Behavior Changes

None. The fix is purely internal DoS mitigation. Legitimate attachments (within configured byte limits) are processed identically.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Explanation: This is a defensive change that hardens parsing against memory exhaustion attacks. It adds pre-allocation size validation but does not expand attack surface. Untrusted inline base64 payloads are now constrained at parse time rather than after unbounded memory allocation.

Repro + Verification

Environment

  • OS: Any (vulnerability is runtime-independent)
  • Runtime/container: Node.js / container with TeamsAttachment handler
  • Integration/channel: MS Teams
  • Relevant config: maxBytes parameter to downloadMSTeamsAttachments() (existing)

Steps

Before fix (vulnerable):

  1. Attacker sends Teams message with HTML attachment containing inline data:image/png;base64, + 1GB base64-encoded payload
  2. Parser calls decodeDataImage()Buffer.from(payload, "base64")
  3. ~750MB memory allocated during decode before any checks
  4. Process memory pressure/crash occurs if system memory exhausted

After fix (hardened):

  1. Same message arrives
  2. Parser calls decodeDataImageWithLimits() with maxInlineBytes: params.maxBytes (e.g., 10MB)
  3. estimateBase64DecodedBytes() returns estimate ~750MB
  4. Check enforces: if (estimatedBytes > opts.maxInlineBytes) return null
  5. Payload is rejected before Buffer.from() allocation
  6. No memory pressure; safe rejection

Expected

  • Large inline base64 payloads rejected at parse time with no memory spike
  • Legitimate inline images within configured limits accepted normally
  • Service continues processing without performance degradation

Actual

✓ Verified in test scenarios; large payloads rejected before allocation

Evidence

  • Code inspection: Size checks validated before Buffer.from() in all paths
  • Type safety verified: decodeDataImageWithLimits() return structure checked
  • Limit flow traced: downloadMSTeamsAttachments()extractInlineImageCandidates(limits)decodeDataImageWithLimits()

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:

    • Large base64 payloads (>configured limits) rejected before decode
    • Legitimate smaller payloads processed normally
    • Cumulative maxInlineTotalBytes limit enforced across multiple inline images
    • Error handling: invalid/non-base64 payloads filtered via isLikelyBase64Payload()
  • Edge cases checked:

    • Empty base64 payload → rejected (decoded size = 0)
    • Payload with whitespace/padding variations → estimated correctly
    • Payload at exact limit boundary → accepted
    • Payload 1 byte over limit → rejected
    • Multiple inline images hitting cumulative limit → stops processing at boundary
  • What you did NOT verify:

    • Cross-OS memory behavior (code is platform-independent)
    • Performance under high message throughput (separate perf testing)
    • Integration with other Teams channels (scope limited to attachment parsing)

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

The limits parameter to extractInlineImageCandidates() is optional; callers not passing it receive default behavior (no limits). Existing call sites must be updated to pass limits for protection (e.g., downloadMSTeamsAttachments() already does).

Failure Recovery (if this breaks)

  • How to disable/revert: Remove limits argument from extractInlineImageCandidates() calls; revert to shared.ts and download.ts to prior versions without size validation
  • Files to restore: extensions/msteams/src/attachments/shared.ts, extensions/msteams/src/attachments/download.ts
  • Bad symptoms to watch for:
    • Legitimate inline images in Teams messages rejected unexpectedly (likely misconfigured maxBytes)
    • Parsing still slow/crashy with large attachments (size limits not threaded through all paths)
    • Type errors on decodeDataImageWithLimits() return structure (ensure callers handle both candidate and estimatedBytes)

Risks and Mitigations

  • Risk: Misconfigured maxBytes limit too small, rejecting legitimate inline images

    • Mitigation: Use existing params.maxBytes passed to downloadMSTeamsAttachments() (already set for other attachments); audit limit value in production config
  • Risk: Estimation math error causes false rejections or false accepts

    • Mitigation: estimateBase64DecodedBytes() conservatively calculates floor((len * 3) / 4) - padding; verified against standard base64 decode formula
  • Risk: Attacker crafts non-base64 payload to bypass isLikelyBase64Payload()

    • Mitigation: Regex ^[A-Za-z0-9+/=\r\n]+$ is strict; invalid base64 still rejected in Buffer.from() catch block (defense-in-depth)
  • Risk: Cumulative limit tracking error across multiple inline images

    • Mitigation: Total tracked as totalEstimatedInlineBytes; boundary check uses estimated bytes before allocation, breaking loop at or before limit

Greptile Summary

Adds memory exhaustion protection to MS Teams inline image parsing by validating base64 payload sizes before allocation. The fix introduces estimateBase64DecodedBytes() to calculate decoded size upfront and decodeDataImageWithLimits() to enforce per-image and cumulative byte limits before calling Buffer.from(). The cumulative limit tracking correctly uses labeled break outerLoop to stop processing when total bytes exceed configured limits. The base64 size estimation formula floor((len * 3) / 4) - padding is mathematically correct and matches actual decoded sizes. Defense-in-depth is maintained with the redundant byteLength check after allocation.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The security fix is well-designed and correctly implemented. The base64 estimation logic is mathematically sound, the cumulative limit tracking with labeled break is correct (previous issue was fixed), and the defense-in-depth approach with redundant checks provides additional safety. No functional bugs or security bypasses found.
  • No files require special attention

Last reviewed commit: 8c32c61

@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S labels Feb 21, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 21, 2026

Additional Comments (1)

extensions/msteams/src/attachments/download.ts
This check is redundant since decodeDataImageWithLimits() already enforces maxInlineBytes and won't return a candidate if the estimated size exceeds the limit (shared.ts:213-214). The inline.data.byteLength here would never exceed params.maxBytes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/attachments/download.ts
Line: 221-223

Comment:
This check is redundant since `decodeDataImageWithLimits()` already enforces `maxInlineBytes` and won't return a candidate if the estimated size exceeds the limit (shared.ts:213-214). The `inline.data.byteLength` here would never exceed `params.maxBytes`.

How can I resolve this? If you propose a fix, please make it concise.

@hackersifu
Copy link
Copy Markdown
Contributor Author

Can you check the latest commit @greptileai

@hackersifu
Copy link
Copy Markdown
Contributor Author

Adding explicit test results from local test:

pnpm exec vitest run --reporter verbose extensions/msteams/src/attachments/shared.test.ts
Planning verbose test run
pnpm exec vitest run --reporter verbose extensions/msteams/src/attachments.test.ts
Reporting test results and push reminder
Verbose test runs completed successfully. Outputs:

RUN v4.0.18 /openclaw

✓ shared.test.ts > msteams attachment allowlists > normalizes wildcard host lists 1ms
✓ shared.test.ts > msteams attachment allowlists > resolves a normalized attachment fetch policy 0ms
✓ shared.test.ts > msteams attachment allowlists > requires https and host suffix match 0ms
✓ shared.test.ts > msteams attachment allowlists > builds shared SSRF policy from suffix allowlist 0ms
✓ shared.test.ts > msteams attachment allowlists > malformed/expanded 999.999.999.999 → true (SDK fails closed) 0ms
✓ shared.test.ts > msteams attachment allowlists > malformed/expanded 256.0.0.1 → true (SDK fails closed) 0ms
✓ shared.test.ts > msteams attachment allowlists > malformed/expanded 10.0.0.256 → true (SDK fails closed) 0ms
✓ shared.test.ts > msteams attachment allowlists > malformed/expanded -1.0.0.1 → false (SDK fails closed) 0ms
✓ shared.test.ts > msteams attachment allowlists > malformed/expanded 1.2.3.4.5 → false (SDK fails closed) 0ms
✓ shared.test.ts > msteams attachment allowlists > malformed/expanded 0:0:0:0:0:0:0:1 → true (SDK fails closed) 1ms
✓ shared.test.ts > resolveAndValidateIP > accepts a hostname resolving to a public IP 0ms
✓ shared.test.ts > resolveAndValidateIP > rejects a hostname resolving to 10.x.x.x 1ms
✓ shared.test.ts > resolveAndValidateIP > rejects a hostname resolving to 169.254.169.254 0ms
✓ shared.test.ts > resolveAndValidateIP > rejects a hostname resolving to loopback 0ms
✓ shared.test.ts > resolveAndValidateIP > rejects a hostname resolving to IPv6 loopback 0ms
✓ shared.test.ts > resolveAndValidateIP > throws on DNS resolution failure 0ms
✓ shared.test.ts > safeFetch > fetches a URL directly when no redirect occurs 1ms
✓ shared.test.ts > safeFetch > follows a redirect to an allowlisted host with public IP 1ms
✓ shared.test.ts > safeFetch > returns the redirect response when dispatcher is provided by an outer guard 0ms
✓ shared.test.ts > safeFetch > still enforces allowlist checks before returning dispatcher-mode redirects 0ms
✓ shared.test.ts > safeFetch > blocks a redirect to a non-allowlisted host 0ms
✓ shared.test.ts > safeFetch > blocks a redirect to an allowlisted host that resolves to a private IP (DNS rebinding) 0ms
✓ shared.test.ts > safeFetch > blocks when the initial URL resolves to a private IP 0ms
✓ shared.test.ts > safeFetch > blocks when initial URL DNS resolution fails 0ms
✓ shared.test.ts > safeFetch > follows multiple redirects when all are valid 0ms
✓ shared.test.ts > safeFetch > throws on too many redirects 0ms
✓ shared.test.ts > safeFetch > blocks redirect to HTTP (non-HTTPS) 0ms
✓ shared.test.ts > safeFetch > strips authorization across redirects outside auth allowlist 0ms
✓ shared.test.ts > attachment fetch auth helpers > sets and clears authorization header by auth allowlist 0ms
✓ shared.test.ts > attachment fetch auth helpers > safeFetchWithPolicy forwards policy allowlists 0ms
✓ shared.test.ts > msteams inline image limits > rejects inline data images above per-image limit 0ms
✓ shared.test.ts > msteams inline image limits > accepts inline data images within limit 0ms
✓ shared.test.ts > msteams inline image limits > enforces cumulative inline size limit across attachments 0ms

Test Files 1 passed (1)
Tests 33 passed (33)

RUN v4.0.18 //openclaw

✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'returns empty string when no attachme…' 1ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'returns empty string when attachments…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'returns image placeholder for one ima…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'returns image placeholder with count …' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'treats Teams file.download.info image…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'returns document placeholder for non-…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'returns document placeholder with cou…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'counts one inline image in html attac…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsAttachmentPlaceholder > 'counts many inline images in html att…' 0ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > 'downloads and stores image contentUrl…' 3ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > 'supports Teams file.download.info dow…' 0ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > 'downloads inline image URLs from html…' 1ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > 'downloads non-image file attachments …' 1ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > stores inline data:image base64 payloads 0ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > 'retries with auth when the first requ…' 1ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > 'skips auth retries when the host is n…' 0ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > preserves auth fallback when dispatcher-mode fetch returns a redirect 1ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > continues scope fallback after non-auth failure and succeeds on later scope 1ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > does not forward Authorization to redirects outside auth allowlist 1ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > skips urls outside the allowlist 0ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsAttachments > blocks redirects to non-https URLs 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsGraphMessageUrls > 'builds channel message urls' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsGraphMessageUrls > 'builds channel reply urls when replyT…' 0ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsGraphMessageUrls > 'builds chat message urls' 0ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsGraphMedia > 'downloads hostedContents images' 50ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsGraphMedia > 'merges SharePoint reference attachmen…' 5ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsGraphMedia > does not forward Authorization for SharePoint redirects outside auth allowlist 6ms
✓ attachments.test.ts > msteams attachments > downloadMSTeamsGraphMedia > blocks SharePoint redirects to hosts outside allowHosts 7ms
✓ attachments.test.ts > msteams attachments > buildMSTeamsMediaPayload > returns single and multi-file fields 1ms

Test Files 1 passed (1)
Tests 29 passed (29)

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 20, 2026
@hackersifu
Copy link
Copy Markdown
Contributor Author

This PR is still active, and I believe it's being added in a future release of multiple MSTeam fixes.

@openclaw-barnacle openclaw-barnacle bot removed the stale Marked as stale due to inactivity label Mar 21, 2026
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 30, 2026
@hackersifu
Copy link
Copy Markdown
Contributor Author

This PR is still active, and I believe it's being added in a future release of multiple MSTeam fixes.

@openclaw-barnacle openclaw-barnacle bot removed the stale Marked as stale due to inactivity label Mar 31, 2026
@BradGroux BradGroux merged commit dd7df07 into openclaw:main Apr 1, 2026
41 checks passed
sasan1200 pushed a commit to sasan1200/openclaw that referenced this pull request Apr 1, 2026
steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants