Skip to content

fix(security): harden server-side SSRF egress filtering across plugins#41962

Open
subrata71 wants to merge 1 commit into
releasefrom
fix/ssrf-egress-hardening-ghsa-batch
Open

fix(security): harden server-side SSRF egress filtering across plugins#41962
subrata71 wants to merge 1 commit into
releasefrom
fix/ssrf-egress-hardening-ghsa-batch

Conversation

@subrata71

@subrata71 subrata71 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Consolidates five HIGH severity SSRF advisories that share a single root cause into one coherent change. Appsmith's centralized outbound-request filter (WebClientUtils) was either gated behind IN_DOCKER (so non-Docker deployments lost address-class filtering) or bypassed entirely by a few plugins that built raw clients.

Advisories addressed: GHSA-7wfp-6c63-99gq (keystone), GHSA-4fjr-w826-cpwm, GHSA-66gg-xpjf-p92v, GHSA-h4wh-59p8-vqff, GHSA-72m2-f9xp-wg9h.

Changes

  1. WebClientUtils — deployment-independent address-class filtering. Link-local (including the full 169.254.0.0/16 IMDS range and IPv6 fe80::/10), any-local, multicast and IPv6 ULA (fc00::/7) are now blocked on every deployment, not just under IN_DOCKER. The cloud-metadata denylist remains always-on. This is the keystone that also closes the loopback-on-non-Docker residual in the ElasticSearch and REST advisories.
  2. Opt-in strict egress mode. APPSMITH_SSRF_BLOCK_PRIVATE_ADDRESS=true additionally blocks loopback and RFC1918 private ranges on all deployments. Default off, so self-hosted deployments that legitimately connect datasources to internal/private hosts are not broken on upgrade.
  3. Consistent plugin egress. The AI plugins (OpenAI, Anthropic, Google AI, Appsmith AI) and the Google Sheets trigger path now build their clients via WebClientUtils.builder() instead of a raw WebClient.builder(), so the SSRF filter applies uniformly. These target fixed provider hosts, so legitimate traffic is unchanged.
  4. SMTP destination validation. The SMTP plugin connects via JavaMail (outside the WebClient pipeline), so the destination host is now validated through the new WebClientUtils.resolveForDatasource() before the session is built — rejecting metadata/link-local targets while allowing legitimate private mail servers by default.

Why this is regression-safe

  • The cloud-metadata denylist behavior is unchanged (already always-on).
  • The only default behavior change for existing traffic is that link-local / any-local / multicast / ULA addresses are now blocked on non-Docker deployments — none of these have a legitimate datasource use.
  • RFC1918 stays reachable by default so existing self-hosted internal datasources keep working; strict blocking is opt-in.
  • Docker deployments keep reaching private-range services (e.g. sibling containers); strict private-range blocking is never gated on IN_DOCKER.

Test plan

  • WebClientUtilsTest — 93 tests pass (added coverage for always-blocked classes regardless of IN_DOCKER, default-allow of loopback/RFC1918, strict-mode blocking via the pure isBlockedResolvedAddress overload, and resolveForDatasource).
  • SmtpPluginTest — added tests that datasourceCreate rejects cloud-metadata and link-local hosts and allows private hosts by default (test module compiles; integration tests require Docker).
  • All six touched plugin modules + appsmith-interfaces compile; spotless:check clean.
  • CI: full server test suite on release.

Automation

/ok-to-test tags="@tag.All"

Tip

🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/28863593712
Commit: bab5d44
Cypress dashboard.
Tags: @tag.All
Spec:


Tue, 07 Jul 2026 12:44:13 UTC

Summary by CodeRabbit

  • New Features

    • Added stricter outbound host validation for supported integrations, helping block unsafe destinations before requests are sent.
    • SMTP datasource setup now surfaces a clear error when the target host is not allowed.
  • Bug Fixes

    • Improved blocking of non-routable addresses, including metadata, loopback, and local/private IP ranges in more cases.
    • Aligned outbound request handling across plugins so safeguards behave consistently for HTTP and datasource connections.

Consolidates five HIGH SSRF advisories that share one root cause — Appsmith's
centralized outbound-request filter (WebClientUtils) is either gated behind
IN_DOCKER or bypassed entirely by some plugins.

- WebClientUtils: apply link-local (incl. the full 169.254.0.0/16 IMDS range),
  any-local, multicast and IPv6 ULA blocking on ALL deployments, not only under
  IN_DOCKER. Cloud-metadata denylist stays always-on. (GHSA-7wfp-6c63-99gq;
  also closes the loopback-on-non-Docker gap in GHSA-66gg-xpjf-p92v and
  GHSA-4fjr-w826-cpwm.)
- Add opt-in strict egress mode APPSMITH_SSRF_BLOCK_PRIVATE_ADDRESS=true that
  additionally blocks loopback and RFC1918 on all deployments. Default off, so
  self-hosted internal datasources are not broken on upgrade. (GHSA-4fjr-w826-cpwm)
- Route the AI plugins (OpenAI/Anthropic/Google AI/Appsmith AI) and the Google
  Sheets trigger path through WebClientUtils.builder() so the SSRF filter is
  applied consistently. (GHSA-h4wh-59p8-vqff)
- Validate the SMTP plugin destination host via the new
  WebClientUtils.resolveForDatasource() before building the JavaMail session,
  rejecting metadata/link-local targets while allowing legitimate private mail
  servers. (GHSA-72m2-f9xp-wg9h)

RFC1918 stays reachable by default so existing self-hosted internal datasources
keep working. Adds regression tests in WebClientUtilsTest and SmtpPluginTest.
@subrata71 subrata71 self-assigned this Jul 7, 2026
@subrata71 subrata71 added the ok-to-test Required label for CI label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

WebClientUtils introduces a strict egress mode (APPSMITH_SSRF_BLOCK_PRIVATE_ADDRESS) and refactors address-class blocking into shared helpers (isBlockedForEgress, isBlockedResolvedAddress) used by both HTTP egress checks and datasource resolution. Multiple plugins switch WebClient construction to WebClientUtils.builder, and SmtpPlugin validates datasource hosts against SSRF rules.

Changes

SSRF Egress Filtering

Layer / File(s) Summary
Strict-egress flag and address-class blocking core
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java
Adds strict-mode flag, refactors always-blocked address classes, and introduces isBlockedForEgress/isBlockedResolvedAddress used by isDisallowedAndFail, requestFilterFn, and resolveForDatasource.
WebClientUtils test coverage
app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java
New parameterized tests cover always-blocked classes, egress checks, resolver behavior, and mode-specific blocking (strict/default/docker).
Plugin WebClient construction via WebClientUtils
.../anthropicPlugin/.../RequestUtils.java, .../appsmithAiPlugin/.../RequestUtils.java, .../googleAiPlugin/.../RequestUtils.java, .../openAiPlugin/.../RequestUtils.java, .../googleSheetsPlugin/.../GoogleSheetsPlugin.java
Replaces WebClient.builder() + ReactorClientHttpConnector with WebClientUtils.builder(...) to route outbound calls through centralized SSRF filtering.
SMTP datasource host validation
.../smtpPlugin/.../SmtpPlugin.java, .../SMTPErrorMessages.java, .../SmtpPluginTest.java
Validates endpoint host via resolveForDatasource in datasourceCreate, adds DS_INVALID_HOST_ERROR_MSG, and adds tests for blocking metadata/link-local hosts while allowing private hosts by default.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • appsmithorg/appsmith#41849: Both PRs modify WebClientUtils' SSRF/IP-literal blocking logic, refactoring the same non-routable address-class filtering helpers used in isDisallowedAndFail and requestFilterFn.

Suggested reviewers: sharat87

Poem

A bunny hops the network wide,
Checking each address, IP by IP,
Loopback, link-local, denied, denied —
WebClientUtils holds the whip. 🐰
Docker or strict, the rules now align,
SMTP too checks before it dials the line. 📬

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main security hardening across plugin SSRF egress filtering.
Description check ✅ Passed The description covers summary, changes, safety, tests, automation, and Cypress results, but omits the required Fixes issue reference and Communication section.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ssrf-egress-hardening-ghsa-batch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/SmtpPlugin.java (1)

204-224: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Pin the SMTP connection to the resolved address
resolveForDatasource() returns an InetAddress, but this code drops it and still configures JavaMail from the raw host string. That leaves a DNS-rebinding window between the guard and the actual SMTP connect/send. Thread the resolved address through the transport setup instead of re-resolving the hostname later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/SmtpPlugin.java`
around lines 204 - 224, The SMTP setup in SmtpPlugin currently validates the
host with WebClientUtils.resolveForDatasource() but then still configures
JavaMail using the raw endpoint.getHost(), leaving a DNS-rebinding gap. Thread
the resolved InetAddress from the guard through the transport/session setup and
use that resolved address for the SMTP connection in the SmtpPlugin flow, so the
same vetted destination is used instead of re-resolving the hostname later.

Source: Learnings

🧹 Nitpick comments (1)
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java (1)

262-288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Near-duplicate of resolveIfAllowed.

resolveForDatasource and resolveIfAllowed (Line 221) differ only in the per-address predicate (isBlockedResolvedAddress vs matchesBlockedAddressClass). Consider extracting the shared normalize/resolve/loop skeleton and parameterizing the predicate to avoid drift between the two resolvers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java`
around lines 262 - 288, `resolveForDatasource` is nearly the same as
`resolveIfAllowed`, with only the per-address block check differing. Extract the
shared normalize/resolve/iterate skeleton into a common helper in
`WebClientUtils`, and pass in the address predicate so `resolveForDatasource`
uses `isBlockedResolvedAddress` while `resolveIfAllowed` uses
`matchesBlockedAddressClass`. Keep the existing
`normalizeHostForComparisonQuietly`, `InetAddress.getAllByName`, and
`DISALLOWED_HOSTS` handling centralized to prevent future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java`:
- Around line 262-288: SMTP hostname validation is still bypassable because
`SmtpPlugin.datasourceCreate` only checks `endpoint.getHost()` before JavaMail
connects. Update the SMTP connection flow to either pin the session/transport to
the resolved `InetAddress` returned by `WebClientUtils.resolveForDatasource` or
add post-connect verification of the remote peer after `connect()`, so the
actual connected address is checked against the DNS-rebinding guard.

In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java`:
- Around line 218-224: The parameterized test in WebClientUtilsTest mixes
loopback and RFC1918 hosts in a case that depends on the runtime IN_DOCKER flag,
so remove the 127.0.0.1 row from
isDisallowedAndFail_allowsLoopbackAndPrivateByDefault and keep only the
deterministic non-Docker-gated private ranges. Leave the separate pure-overload
coverage for isDisallowedAndFail and resolveForDatasource intact so the test
stays stable regardless of environment.

---

Outside diff comments:
In
`@app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/SmtpPlugin.java`:
- Around line 204-224: The SMTP setup in SmtpPlugin currently validates the host
with WebClientUtils.resolveForDatasource() but then still configures JavaMail
using the raw endpoint.getHost(), leaving a DNS-rebinding gap. Thread the
resolved InetAddress from the guard through the transport/session setup and use
that resolved address for the SMTP connection in the SmtpPlugin flow, so the
same vetted destination is used instead of re-resolving the hostname later.

---

Nitpick comments:
In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java`:
- Around line 262-288: `resolveForDatasource` is nearly the same as
`resolveIfAllowed`, with only the per-address block check differing. Extract the
shared normalize/resolve/iterate skeleton into a common helper in
`WebClientUtils`, and pass in the address predicate so `resolveForDatasource`
uses `isBlockedResolvedAddress` while `resolveIfAllowed` uses
`matchesBlockedAddressClass`. Keep the existing
`normalizeHostForComparisonQuietly`, `InetAddress.getAllByName`, and
`DISALLOWED_HOSTS` handling centralized to prevent future drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4bfa7f2e-d533-4b60-96be-50e850818978

📥 Commits

Reviewing files that changed from the base of the PR and between 7491d32 and bab5d44.

📒 Files selected for processing (10)
  • app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java
  • app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java
  • app/server/appsmith-plugins/anthropicPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java
  • app/server/appsmith-plugins/appsmithAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java
  • app/server/appsmith-plugins/googleAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java
  • app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java
  • app/server/appsmith-plugins/openAiPlugin/src/main/java/com/external/plugins/utils/RequestUtils.java
  • app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/SmtpPlugin.java
  • app/server/appsmith-plugins/smtpPlugin/src/main/java/com/external/plugins/exceptions/SMTPErrorMessages.java
  • app/server/appsmith-plugins/smtpPlugin/src/test/java/com/external/plugins/SmtpPluginTest.java

Comment on lines +262 to +288
public static Optional<InetAddress> resolveForDatasource(String host) {
if (!StringUtils.hasText(host)) {
return Optional.empty();
}

final String canonicalHost = normalizeHostForComparisonQuietly(host);
if (DISALLOWED_HOSTS.contains(canonicalHost)) {
return Optional.empty();
}

final InetAddress[] resolved;
try {
resolved = InetAddress.getAllByName(host);
} catch (UnknownHostException e) {
return Optional.empty();
}

final boolean inDocker = "1".equals(System.getenv("IN_DOCKER"));
for (InetAddress addr : resolved) {
if (DISALLOWED_HOSTS.contains(normalizeHostForComparisonQuietly(addr.getHostAddress()))
|| isBlockedResolvedAddress(addr, inDocker, BLOCK_PRIVATE_ADDRESSES)) {
return Optional.empty();
}
}

return Optional.of(resolved[0]);
}

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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# How does SmtpPlugin build the JavaMail session after resolveForDatasource? Does it pin the IP?
fd 'SmtpPlugin.java' --exec sed -n '190,320p' {}

Repository: appsmithorg/appsmith

Length of output: 6707


🌐 Web query:

JavaMail Transport.connect host DNS resolution re-resolves on connect mail.smtp.host ip pinning

💡 Result:

In Jakarta Mail (formerly JavaMail), the Transport.connect method does not explicitly pin IP addresses, nor does it inherently cache DNS lookups in a way that prevents re-resolution [1][2]. The resolution of the mail.smtp.host property is handled by the underlying Java networking stack (specifically java.net.InetAddress) each time a new connection is established [1][3]. Key technical points regarding DNS resolution and IP pinning in this context include: 1. DNS Resolution Behavior: When you call transport.connect, Jakarta Mail triggers a socket connection attempt to the host specified in the mail.smtp.host property (or passed as an argument) [4][5]. This relies on the standard Java networking library to resolve the hostname to an IP address [1][2]. If the hostname resolves to multiple IP addresses, the behavior depends on the JVM's underlying implementation of InetAddress.getByName or getAllByName, which typically follows the OS-level DNS resolution rules and potential local JVM-level caching [1]. 2. Lack of Native IP Pinning: Jakarta Mail does not implement its own internal DNS caching layer or IP pinning mechanism [1]. Because the library relies on the Java runtime's networking facilities, it does not "pin" to a specific IP address across multiple sessions unless explicitly configured to do so by the network environment or JVM system properties [1][3]. 3. DNS Timeouts and Performance: DNS resolution is performed synchronously. If the DNS server is unresponsive or slow, the connect call can appear to hang, as JavaMail timeouts generally cover only the socket connection, read, and write operations, not the initial DNS lookup [3]. 4. Workarounds for Resilience: Because Jakarta Mail does not natively handle DNS-based load balancing (such as iterating through multiple IP addresses from an MX record or a round-robin DNS entry upon connection failure), developers often implement custom retry logic [1][2]. This involves resolving the hostname manually via InetAddress.getAllByName, shuffling the resulting array of IP addresses, and attempting to connect to each address sequentially if the previous attempt fails [1]. In summary, JavaMail performs a fresh DNS lookup through the standard Java networking stack at the time of connection [1][3]. If you require behavior like "IP pinning" (sticking to one IP) or conversely "DNS failover" (trying all IPs), these must be managed at the application layer or via JVM configuration [1][2].

Citations:


SMTP still needs IP pinning or post-connect validation
SmtpPlugin.datasourceCreate validates endpoint.getHost() up front, but JavaMail still connects using the hostname, so the SMTP transport can re-resolve it at connect time and bypass the DNS-rebinding guard. Pin the session to the resolved InetAddress, or validate the remote peer after connect().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java`
around lines 262 - 288, SMTP hostname validation is still bypassable because
`SmtpPlugin.datasourceCreate` only checks `endpoint.getHost()` before JavaMail
connects. Update the SMTP connection flow to either pin the session/transport to
the resolved `InetAddress` returned by `WebClientUtils.resolveForDatasource` or
add post-connect verification of the remote peer after `connect()`, so the
actual connected address is checked against the DNS-rebinding guard.

Source: Learnings

Comment on lines +218 to +224
@ParameterizedTest
@ValueSource(strings = {"127.0.0.1", "10.0.0.1", "192.168.1.1", "172.16.0.1"})
public void isDisallowedAndFail_allowsLoopbackAndPrivateByDefault(String host) {
assertFalse(
WebClientUtils.isDisallowedAndFail(host, null),
"Expected " + host + " to be allowed by default (no IN_DOCKER, strict egress off)");
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does the CI/build env set IN_DOCKER for server test runs?
rg -n 'IN_DOCKER' --iglob '*.yml' --iglob '*.yaml' --iglob 'Dockerfile*' --iglob '*.sh'

Repository: appsmithorg/appsmith

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate IN_DOCKER usage ---'
rg -n 'IN_DOCKER' app/server || true

echo
echo '--- inspect WebClientUtilsTest around the flagged lines ---'
sed -n '180,260p' app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java

echo
echo '--- inspect WebClientUtils implementation ---'
fd -a 'WebClientUtils.java' app/server

Repository: appsmithorg/appsmith

Length of output: 6630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd -a 'WebClientUtils.java' app/server | head -n 1)"
echo "FILE=$file"
echo
ast-grep outline "$file" --view expanded || true
echo
sed -n '1,260p' "$file"

Repository: appsmithorg/appsmith

Length of output: 13740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java"

echo '--- isDisallowedAndFail / resolveForDatasource / egress checks ---'
sed -n '250,420p' "$file"

Repository: appsmithorg/appsmith

Length of output: 7944


Drop the loopback rows from the live-env tests
isDisallowedAndFail and resolveForDatasource both read IN_DOCKER at runtime, so the 127.0.0.1 cases flip under IN_DOCKER=1 while the RFC1918 rows stay deterministic. Keep the pure overload coverage below and leave only the non-Docker-gated cases here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java`
around lines 218 - 224, The parameterized test in WebClientUtilsTest mixes
loopback and RFC1918 hosts in a case that depends on the runtime IN_DOCKER flag,
so remove the 127.0.0.1 row from
isDisallowedAndFail_allowsLoopbackAndPrivateByDefault and keep only the
deterministic non-Docker-gated private ranges. Leave the separate pure-overload
coverage for isDisallowedAndFail and resolveForDatasource intact so the test
stays stable regardless of environment.

@github-actions

Copy link
Copy Markdown

This PR has not seen activitiy for a while. It will be closed in 7 days unless further activity is detected.

@github-actions github-actions Bot added the Stale label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok-to-test Required label for CI Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant