Skip to content

fix(msteams): add SSRF validation to file consent upload URL#23596

Merged
BradGroux merged 3 commits intoopenclaw:mainfrom
lewiswigmore:security/file-consent-ssrf-validation
Apr 6, 2026
Merged

fix(msteams): add SSRF validation to file consent upload URL#23596
BradGroux merged 3 commits intoopenclaw:mainfrom
lewiswigmore:security/file-consent-ssrf-validation

Conversation

@lewiswigmore
Copy link
Copy Markdown
Contributor

@lewiswigmore lewiswigmore commented Feb 22, 2026

Summary

uploadToConsentUrl() in the MS Teams plugin performs a PUT request to the URL provided in the fileConsent/invoke response without any validation. While the invoke activity is authenticated via JWT (Bot Framework webhook auth), a malicious user within an authenticated Teams tenant can craft an invoke with an attacker-controlled uploadUrl, causing the bot to PUT file data to arbitrary destinations — a server-side request forgery (SSRF).

Attack scenario

  1. Attacker sends a message triggering a large-file upload flow
  2. Bot sends a FileConsentCard with a pending upload stored in memory
  3. Attacker crafts a fileConsent/invoke activity with action: "accept" and uploadInfo.uploadUrl pointing to an internal service (e.g. http://169.254.169.254/latest/meta-data/ or an internal API)
  4. Bot performs a PUT request to the attacker-chosen URL with the file buffer as the body

Fix

This PR adds validateConsentUploadUrl() which enforces three checks before any fetch occurs:

  1. HTTPS-only — rejects http://, file://, and other protocols
  2. Hostname allowlist — a new CONSENT_UPLOAD_HOST_ALLOWLIST restricted to Microsoft/SharePoint domains that Teams legitimately provides as upload destinations (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
  3. DNS resolution check — resolves the hostname and rejects private/reserved IPs (RFC 1918, loopback 127.0.0.0/8, link-local 169.254.0.0/16, IPv6 ::1, fe80::/10, fc00::/7) to prevent DNS rebinding attacks

The allowlist is intentionally narrower than DEFAULT_MEDIA_HOST_ALLOWLIST, which includes overly broad domains like blob.core.windows.net and trafficmanager.net that any Azure customer can create endpoints under.

Testing

47 new tests covering:

  • IPv4/IPv6 private IP detection (24 cases)
  • Protocol enforcement, hostname allowlist matching, suffix-trick rejection
  • DNS failure handling
  • End-to-end uploadToConsentUrl() integration verifying fetch is never called for blocked URLs

All 201 existing msteams tests continue to pass with zero regressions.

Greptile Summary

Adds SSRF protection to MS Teams file consent upload flow by validating URLs before fetch with HTTPS enforcement, Microsoft/SharePoint domain allowlist, and DNS resolution checks to block private IPs. The fix addresses a real security vulnerability where authenticated Teams users could trigger bot PUT requests to arbitrary internal endpoints.

Key changes:

  • New validateConsentUploadUrl() enforces protocol, hostname allowlist, and DNS-based IP checks
  • CONSENT_UPLOAD_HOST_ALLOWLIST restricts uploads to legitimate Microsoft domains
  • isPrivateOrReservedIP() blocks RFC 1918, loopback, and link-local ranges
  • 47 new tests with comprehensive coverage of validation logic
  • Integration point in uploadToConsentUrl() ensures all uploads are validated

Critical issues found:

  • IPv6 validation has gaps: IPv4-mapped addresses like ::ffff:10.0.0.1 bypass private IP detection
  • IPv4 validation doesn't verify octets are in 0-255 range, allowing malformed IPs through

Both issues could allow SSRF attacks to succeed despite the validation layer.

Confidence Score: 2/5

  • This PR has critical security gaps in IPv6 and IPv4 validation that could allow SSRF bypass
  • Score reflects two confirmed logical errors in the IP validation layer that directly undermine the SSRF protection: (1) IPv4-mapped IPv6 addresses can bypass private IP detection, and (2) invalid IPv4 octets aren't validated. Both issues create attack vectors that defeat the security fix
  • Pay close attention to extensions/msteams/src/file-consent.ts - the IP validation logic needs fixes before merge

Last reviewed commit: f260f9a

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: M labels Feb 22, 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

Comment on lines +57 to +67
// IPv6 checks
const normalized = ip.toLowerCase();
// ::1 loopback
if (normalized === "::1") return true;
// fe80::/10 link-local
if (normalized.startsWith("fe80:") || normalized.startsWith("fe80")) return true;
// fc00::/7 unique-local (fc00:: and fd00::)
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
// :: unspecified
if (normalized === "::") return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IPv6 detection incomplete - doesn't handle IPv4-embedded addresses like ::ffff:127.0.0.1 which bypass validation

Suggested change
// IPv6 checks
const normalized = ip.toLowerCase();
// ::1 loopback
if (normalized === "::1") return true;
// fe80::/10 link-local
if (normalized.startsWith("fe80:") || normalized.startsWith("fe80")) return true;
// fc00::/7 unique-local (fc00:: and fd00::)
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
// :: unspecified
if (normalized === "::") return true;
// IPv6 checks
const normalized = ip.toLowerCase();
// Handle IPv4-embedded IPv6 (e.g., ::ffff:127.0.0.1, ::ffff:10.0.0.1)
const ipv4MappedMatch = /::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
if (ipv4MappedMatch) {
return isPrivateOrReservedIP(ipv4MappedMatch[1]);
}
// ::1 loopback
if (normalized === "::1") return true;
// fe80::/10 link-local
if (normalized.startsWith("fe80:") || normalized.startsWith("fe80")) return true;
// fc00::/7 unique-local (fc00:: and fd00::)
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
// :: unspecified
if (normalized === "::") return true;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/file-consent.ts
Line: 57-67

Comment:
IPv6 detection incomplete - doesn't handle IPv4-embedded addresses like `::ffff:127.0.0.1` which bypass validation

```suggestion
  // IPv6 checks
  const normalized = ip.toLowerCase();
  
  // Handle IPv4-embedded IPv6 (e.g., ::ffff:127.0.0.1, ::ffff:10.0.0.1)
  const ipv4MappedMatch = /::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
  if (ipv4MappedMatch) {
    return isPrivateOrReservedIP(ipv4MappedMatch[1]);
  }
  
  // ::1 loopback
  if (normalized === "::1") return true;
  // fe80::/10 link-local
  if (normalized.startsWith("fe80:") || normalized.startsWith("fe80")) return true;
  // fc00::/7 unique-local (fc00:: and fd00::)
  if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
  // :: unspecified
  if (normalized === "::") return true;
```

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

Comment on lines +38 to +55
// IPv4 checks
const v4Parts = ip.split(".");
if (v4Parts.length === 4) {
const a = Number(v4Parts[0]);
const b = Number(v4Parts[1]);
// 10.0.0.0/8
if (a === 10) return true;
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true;
// 192.168.0.0/16
if (a === 192 && b === 168) return true;
// 127.0.0.0/8 (loopback)
if (a === 127) return true;
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254) return true;
// 0.0.0.0/8
if (a === 0) return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IPv4 validation doesn't verify octets are 0-255, allowing invalid IPs like 999.999.999.999 to pass through

Suggested change
// IPv4 checks
const v4Parts = ip.split(".");
if (v4Parts.length === 4) {
const a = Number(v4Parts[0]);
const b = Number(v4Parts[1]);
// 10.0.0.0/8
if (a === 10) return true;
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true;
// 192.168.0.0/16
if (a === 192 && b === 168) return true;
// 127.0.0.0/8 (loopback)
if (a === 127) return true;
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254) return true;
// 0.0.0.0/8
if (a === 0) return true;
}
// IPv4 checks
const v4Parts = ip.split(".");
if (v4Parts.length === 4) {
const octets = v4Parts.map(Number);
// Validate all parts are valid 0-255
if (octets.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
return false;
}
const [a, b] = octets;
// 10.0.0.0/8
if (a === 10) return true;
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true;
// 192.168.0.0/16
if (a === 192 && b === 168) return true;
// 127.0.0.0/8 (loopback)
if (a === 127) return true;
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254) return true;
// 0.0.0.0/8
if (a === 0) return true;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/file-consent.ts
Line: 38-55

Comment:
IPv4 validation doesn't verify octets are 0-255, allowing invalid IPs like `999.999.999.999` to pass through

```suggestion
  // IPv4 checks
  const v4Parts = ip.split(".");
  if (v4Parts.length === 4) {
    const octets = v4Parts.map(Number);
    // Validate all parts are valid 0-255
    if (octets.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
      return false;
    }
    
    const [a, b] = octets;
    // 10.0.0.0/8
    if (a === 10) return true;
    // 172.16.0.0/12
    if (a === 172 && b >= 16 && b <= 31) return true;
    // 192.168.0.0/16
    if (a === 192 && b === 168) return true;
    // 127.0.0.0/8 (loopback)
    if (a === 127) return true;
    // 169.254.0.0/16 (link-local)
    if (a === 169 && b === 254) return true;
    // 0.0.0.0/8
    if (a === 0) return true;
  }
```

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

@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 Feb 28, 2026
@lewiswigmore
Copy link
Copy Markdown
Contributor Author

The failing CI check (checks-windows) is unrelated to this PR — it's 3 tests in test/media-understanding.auto.test.ts (sherpa-onnx, whisper-cli, gemini CLI) that appear to be broken on the Windows runner independently. All 4804 other tests pass, including the 47 new file-consent tests.

Can a maintainer re-run or skip the flaky check so this can move forward?

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Mar 3, 2026
@BradGroux BradGroux self-assigned this Mar 10, 2026
@BradGroux
Copy link
Copy Markdown
Member

Hi @lewiswigmore — thanks for the submission. I’m the new Microsoft Teams maintainer for OpenClaw. Please give me a day or two to work through the open Teams backlog. Also, join the Twitter community for daily MS Teams feedback + updates: https://x.com/i/communities/2031170403607974228

1 similar comment
@BradGroux
Copy link
Copy Markdown
Member

Hi @lewiswigmore — thanks for the submission. I’m the new Microsoft Teams maintainer for OpenClaw. Please give me a day or two to work through the open Teams backlog. Also, join the Twitter community for daily MS Teams feedback + updates: https://x.com/i/communities/2031170403607974228

@lewiswigmore
Copy link
Copy Markdown
Contributor Author

lewiswigmore commented Apr 6, 2026

Hey @BradGroux - just checking in on this one. Let me know if there's anything else needed from my side.

@BradGroux
Copy link
Copy Markdown
Member

BradGroux commented Apr 6, 2026

Nice fix overall. I pushed a follow-up hardening commit.

  • Validate all DNS answers for consent upload hostnames, not just a single lookup result, and block if any answer is private or reserved.
  • Added tests for mixed DNS answers (public + private) and all-public multi-answer success.
  • Added the msteams changelog entry under Unreleased.

This closes a potential bypass where one resolved answer is public but another is private or internal.

Commit: 51090471ec

lewiswigmore and others added 2 commits April 6, 2026 08:28
The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.
@BradGroux BradGroux force-pushed the security/file-consent-ssrf-validation branch from 5109047 to 90592be Compare April 6, 2026 13:31
@BradGroux BradGroux merged commit 1234c87 into openclaw:main Apr 6, 2026
20 of 35 checks passed
steipete pushed a commit to leoleedev/openclaw that referenced this pull request Apr 6, 2026
…w#23596)

* fix(msteams): add SSRF validation to file consent upload URL

The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.

* fix(msteams): validate all DNS answers for consent uploads

* fix(msteams): restore changelog header

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Mlightsnow pushed a commit to Mlightsnow/openclaw that referenced this pull request Apr 6, 2026
…w#23596)

* fix(msteams): add SSRF validation to file consent upload URL

The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.

* fix(msteams): validate all DNS answers for consent uploads

* fix(msteams): restore changelog header

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…w#23596)

* fix(msteams): add SSRF validation to file consent upload URL

The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.

* fix(msteams): validate all DNS answers for consent uploads

* fix(msteams): restore changelog header

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…w#23596)

* fix(msteams): add SSRF validation to file consent upload URL

The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.

* fix(msteams): validate all DNS answers for consent uploads

* fix(msteams): restore changelog header

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…w#23596)

* fix(msteams): add SSRF validation to file consent upload URL

The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.

* fix(msteams): validate all DNS answers for consent uploads

* fix(msteams): restore changelog header

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
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: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants