Skip to content

fix(security): block Redis datasource from internal Appsmith Redis (GHSA-qhfj-g87x-m39w)#41921

Open
wyattwalter wants to merge 12 commits into
releasefrom
fix/ssrf-redis-host-filter
Open

fix(security): block Redis datasource from internal Appsmith Redis (GHSA-qhfj-g87x-m39w)#41921
wyattwalter wants to merge 12 commits into
releasefrom
fix/ssrf-redis-host-filter

Conversation

@wyattwalter

@wyattwalter wyattwalter commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Mitigates GHSA-qhfj-g87x-m39w. The Redis datasource plugin opened a Jedis connection to a user-controlled host/port with no IP-class filtering, so a user with datasource-create permission could point a Redis datasource at the internal Appsmith Redis. The existing HTTP-side filter doesn't cover Redis — Jedis uses the JVM's default resolver and bypasses the Reactor Netty hook.

This PR also closes a DNS-rebinding TOCTOU on the same path and enforces TLS certificate hostname verification for rediss:// datasources (items 4 and 5 below).

Changes

  1. Block Redis datasources from reaching the internal Appsmith Redis. RedisPlugin.datasourceCreate now consults a shared RestrictedHostFilter. Blocks literal hostname / IP matches against APPSMITH_REDIS_URL and APPSMITH_REDIS_GIT_URL, plus loopback / link-local / cloud-metadata literals. Enforcement is connection-time only — PluginExecutor.validateDatasource is documented format-only, so host-policy belongs on the connection path. UX: Save succeeds, "Test Datasource" rejects with Host not allowed..

  2. Make SSRF protections always-on; rename IN_DOCKER to APPSMITH_DISABLE_SSRF_FILTER. The previous filter gated the loopback / link-local block on IN_DOCKER=1, which Appsmith's official Docker image always sets — so users on documented deployment paths were already getting this protection. This change closes the gap for unofficial distributions (self-built images, bare-metal, etc.) and replaces a poorly-named variable with one that actually describes what it does. The most likely place to hit this is a development machine running the server directly against http://localhost:* — set APPSMITH_DISABLE_SSRF_FILTER=true to allow it.

  3. Extract RestrictedHostFilter from WebClientUtils. Pure refactor — the host-filter logic had already grown several non-HTTP call sites and the class name had outgrown its scope.

  4. Close the DNS-rebinding TOCTOU on the Redis path (upgrade Jedis 3.3.0 → 5.2.0 + pin the resolved IP). The create-time pre-check in (1) and Jedis's own connection-time lookup resolved the hostname independently, so a hostile resolver could return an allowed IP to the check and the internal Redis IP to the driver. Jedis 5.2.0 (up from 3.3.0, which predated the API) allows injecting a JedisSocketFactory; the new RestrictedHostJedisSocketFactory resolves once at connect time, validates every resolved address, and connects directly to the pinned IP — so the driver never re-resolves and the window is closed. The create-time check stays as fast Save/Test UX and now logs the block at WARN, tagged with the requesting user/org/trace (a useful reconnaissance signal). Jedis is plugin-isolated (PF4J + shaded); the server's own Redis runs on Lettuce and is untouched.

  5. Enforce TLS certificate hostname verification for rediss:// datasources. Jedis 5.2.0 validates the certificate trust chain but does not verify that the cert's SAN/CN matches the host (it only does so by default in 7.5.0+), so a man-in-the-middle presenting any CA-trusted-but-mismatched cert on the pinned IP would have been accepted. The socket factory now sets the endpoint identification algorithm to HTTPS, verifying against the configured hostname (we connect to the pinned IP but hand the original hostname to the TLS layer for SNI and verification). This was a pre-existing gap, not introduced here; closing it now is low-risk because Redis TLS support is recent (feat: Redis datasource: add TLS (SSL mode) support in backend + datasource UI #41587, ~2026-03) and barely adopted, and chain validation was already on — so only CA-trusted-but-mismatched certs and IP-addressed rediss:// (certs rarely carry IP SANs) are newly rejected.

Test plan

  • CI green across all commits (Jedis 5.2.0 upgrade, socket factory, TLS verification, logging).
  • Deploy preview: Redis datasource at `localhost` / in-cluster Redis service DNS — Save accepted, Test rejected with `Host not allowed.`. Legitimate external Redis works.
  • Connect-time SSRF rejection (the rebinding gate) covered by RedisPluginTest (socketFactory_blockedHost_failsAtConnectTime). A live end-to-end rebinding flip needs a changing resolver and was not exercised on the DP (rbndr.us is unreachable from AWS).
  • Confirm the one existing rediss:// user (Appsmith internal) connects via a hostname matching its cert, now that hostname verification is enforced.
  • Dev environment: confirm `APPSMITH_DISABLE_SSRF_FILTER=true` unblocks `http://localhost:*\` datasources during local development.

🤖 Generated with Claude Code

Warning

Tests have not run on the HEAD bfef59b yet


Thu, 25 Jun 2026 20:39:48 UTC

Summary by CodeRabbit

Release Notes

  • New Features

    • Added centralized SSRF/restricted host filtering with denylisting and normalization.
    • Enabled consistent enforcement across outbound web requests, Elasticsearch DNS resolution, SMTP test-email, and Redis datasource connections.
    • Mitigated Redis DNS rebinding by validating once and pinning the resolved address.
  • Bug Fixes

    • Blocked hosts now fail with a consistent “Host not allowed” error across execution paths.
  • Tests

    • Added dedicated host/IP blocking test suite.
    • Updated integration tests to explicitly control SSRF filtering enablement and state restoration during runs.

…HSA-qhfj-g87x-m39w)

The Redis datasource plugin opened a Jedis connection to a user-controlled
host/port with no IP-class filtering, so a user with datasource-create
permission could point a Redis datasource at the internal Appsmith Redis.
The existing HTTP-side filter doesn't cover Redis — Jedis uses the JVM's
default resolver and bypasses the Reactor Netty hook.

Three changes:

1. Block Redis datasources from reaching the internal Appsmith Redis or
   non-routable addresses. RedisPlugin.datasourceCreate now consults a
   shared RestrictedHostFilter — blocks hostname / IP overlap with
   APPSMITH_REDIS_URL and APPSMITH_REDIS_GIT_URL, plus loopback /
   link-local / cloud-metadata. Enforcement is connection-time only;
   PluginExecutor.validateDatasource is documented format-only, so
   host-policy belongs on the connection path. UX: Save succeeds, Test
   Datasource rejects with "Host not allowed.".

2. Make SSRF protections always-on; rename IN_DOCKER to
   APPSMITH_DISABLE_SSRF_FILTER. The previous filter gated loopback /
   link-local on IN_DOCKER=1, which the official Docker image always
   sets, so users on documented deployment paths were already getting
   this protection. Closes the gap for unofficial distributions (self-
   built images, bare-metal, etc.) and replaces a poorly-named variable.
   The most likely place to hit this is a dev machine running the server
   directly against http://localhost:* — set
   APPSMITH_DISABLE_SSRF_FILTER=true to allow it.

3. Extract RestrictedHostFilter from WebClientUtils. Pure refactor —
   the host-filter logic had already grown several non-HTTP call sites.
@wyattwalter wyattwalter requested a review from sharat87 as a code owner June 23, 2026 18:34
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduces RestrictedHostFilter, a new utility centralizing all SSRF host-blocking logic previously embedded in WebClientUtils. WebClientUtils now delegates to it. RedisPlugin.datasourceCreate, HttpAsyncClientBuilder's DNS resolver, and EnvManagerCEImpl.sendTestEmail are updated to call the new filter directly. A new RestrictedHostJedisSocketFactory enforces DNS pinning at Redis socket-open time. A Surefire kill-switch disables filtering JVM-wide during tests, with individual SSRF regression tests re-enabling it per method. The Redis plugin dependency is updated to v5.2.0.

Changes

SSRF Filter Extraction and Plugin Wiring

Layer / File(s) Summary
RestrictedHostFilter class structure and static initialization
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java
Defines the new RestrictedHostFilter class with documentation, kill-switch flag, static denylist computation (cloud-metadata IPs, loopback literals, and normalized IPv4-mapped/compatible IPv6 conversion), and internal Redis hostname initialization from APPSMITH_REDIS_URL and APPSMITH_REDIS_GIT_URL environment variables.
RestrictedHostFilter host blocking methods and helpers
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java
Implements DNS-aware (isHostBlocked), DNS-free (isLiteralBlocked), resolution-return (resolveIfAllowed), promise-fail (isDisallowedAndFail), and Redis-specific (firstAllowedRedisAddress) blocking paths; IP-class validators; and test-mutation APIs for kill-switch, internal Redis hosts, and allowlist; plus IP validation and canonicalization helpers.
WebClientUtils delegates SSRF enforcement to RestrictedHostFilter
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java
Removes in-class denylist computation, HOST_NOT_ALLOWED constant, resolveIfAllowed, and isDisallowedAndFail; updates requestFilterFn and both NameResolver resolve paths (doResolve, doResolveAll) to call RestrictedHostFilter equivalents.
RestrictedHostJedisSocketFactory for Redis connection-time enforcement
app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RestrictedHostJedisSocketFactory.java
Introduces a custom JedisSocketFactory that performs single DNS resolution, validates pinned IPs via RestrictedHostFilter.firstAllowedRedisAddress, connects directly to validated IP literal to prevent DNS rebinding TOCTOU, and handles TLS with SNI and hostname verification using the original configured hostname.
RedisPlugin datasource creation host enforcement and pool construction
app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
Adds assertHostAllowed helper to datasourceCreate that calls RestrictedHostFilter.isHostBlocked and throws AppsmithPluginException with HOST_NOT_ALLOWED on denial; replaces JedisPool construction to use RestrictedHostJedisSocketFactory for DNS pinning; updates validateDatasource comments to document that host enforcement is deferred to datasourceCreate.
ElasticSearch DNS resolver and EnvManager SMTP host validation
app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.java, app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCEImpl.java
Swaps WebClientUtils references for RestrictedHostFilter.isDisallowedAndFail in HttpAsyncClientBuilder DNS resolver for both input host and resolved IPs; switches EnvManagerCEImpl.sendTestEmail to use RestrictedHostFilter.resolveIfAllowed for SMTP host resolution and validation.
Maven Surefire kill-switch configuration
app/server/pom.xml
Adds appsmith.test.bypass.ssrf=true system property to maven-surefire-plugin configuration so SSRF filtering is disabled JVM-wide during test runs by default, with per-class/method re-enablement where needed.
RestrictedHostFilterTest unit coverage and WebClientUtilsTest migration
app/server/appsmith-interfaces/src/test/java/com/appsmith/util/RestrictedHostFilterTest.java, ...WebClientUtilsTest.java, ...OAuth2ClientCredentialsTest.java
Adds comprehensive RestrictedHostFilterTest covering all public blocking methods and kill-switch regressions; migrates WebClientUtilsTest to reference RestrictedHostFilter.HOST_NOT_ALLOWED and adds per-class filter lifecycle hooks; adds explanatory comment in OAuth2ClientCredentialsTest.
Plugin SSRF integration tests with per-method filter state management
app/server/appsmith-plugins/elasticSearchPlugin/src/test/..., ...graphqlPlugin/src/test/..., ...redisPlugin/src/test/..., ...restApiPlugin/src/test/..., app/server/appsmith-server/src/test/.../EnvManagerTest.java
Updates all SSRF regression tests to explicitly re-enable RestrictedHostFilter via setSsrfFilterDisabledForTesting(false), use setAlwaysAllowedHostsForTesting for loopback/MockWebServer cases, and restore state in finally blocks; adds a new Redis integration test asserting blocked-host failure with HOST_NOT_ALLOWED.
Dependency version updates
app/server/appsmith-plugins/redisPlugin/pom.xml
Updates redis.clients:jedis from 3.3.0 to 5.2.0.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • appsmithorg/appsmith#41849: Adds isBlockedIpAddressClass/matchesBlockedAddressClass and non-routable IP class denylist logic directly in WebClientUtils — the exact methods this PR extracts into RestrictedHostFilter.

Suggested labels

ok-to-test, Bug

Suggested reviewers

  • subrata71

Poem

🛡️ The filter once scattered, now stands in one place,
RestrictedHostFilter guards each loopback and space.
No metadata IP shall sneak through the gate,
Redis, ElasticSearch—all blocked when too late.
The JedisSocketFactory pins IPs with care,
SSRF beware—Appsmith's fortress is fair! 🔒

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description covers summary, changes, and test plan, but it misses the required issue reference, /ok-to-test tag, and communication checkbox sections. Add a Fixes issue reference, the /ok-to-test automation tag, and the Communication checkbox; include any relevant links or dependencies if applicable.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: implementing SSRF mitigation for the Redis datasource plugin to block access to internal Appsmith Redis, addressing security vulnerability GHSA-qhfj-g87x-m39w.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-redis-host-filter

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: 5

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/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java (1)

262-290: 🔒 Security & Privacy | 🟠 Major

Prevent DNS rebinding between assertHostAllowed and socket creation.

The hostname is validated in assertHostAllowed before JedisPool construction, but the actual TCP connection occurs later when getResource() is called. An attacker-controlled hostname that passes validation could resolve to an internal IP address at connection time. Since Jedis doesn't support pre-resolved IP injection, add post-connect validation immediately after getResource() to verify the remote address matches the checked hostname, or document the deliberate TOCTOU acceptance with risk justification.

🤖 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/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java`
around lines 262 - 290, The hostname is validated in `assertHostAllowed()`
before JedisPool creation, but the actual TCP connection occurs later when
`getResource()` is called on the JedisPool instance. An attacker-controlled
hostname could resolve to a different IP address between validation and
connection time. Locate all places where `getResource()` is called on the
JedisPool and add post-connection validation immediately after to verify that
the remote peer address matches the hostname that was validated in
`assertHostAllowed()`, or alternatively document the deliberate
Time-Of-Check-Time-Of-Use acceptance with clear risk justification if you decide
to accept this DNS rebinding risk.

Source: Learnings

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

338-355: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: resolveInternalRedisIps re-resolves on every isHostBlocked call.

Each Redis connection attempt triggers a fresh DNS lookup per internal Redis host. The internal hosts are effectively static, so a short-TTL cache (or one-time resolve with periodic refresh) would trim avoidable lookups on the connection path. Not urgent given this runs on boundedElastic.

🤖 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/RestrictedHostFilter.java`
around lines 338 - 355, The resolveInternalRedisIps method performs DNS lookups
repeatedly on every isHostBlocked call, which is inefficient since Redis hosts
are static. Implement caching for the DNS resolution results by storing the
resolved IPs in a cached field that is computed once during initialization or on
first use, rather than recalculating them on each method invocation. Consider
using a memoization approach or a class-level cache that stores the result of
resolveInternalRedisIps so that subsequent calls to isHostBlocked can reuse the
cached IP set instead of triggering fresh DNS lookups.

165-201: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Normalize internal-Redis hosts the same way you compare them.

computeInternalRedisHosts/addInternalRedisHostFromEnv/setInternalRedisHostsForTesting store hosts as trim().toLowerCase(Locale.ROOT) only, but isHostBlocked/isLiteralBlocked compare against the fully canonicalized form (normalizeHostForComparison* — decorator-stripped, IP-normalized). Two mismatches fall out:

  • An IPv6 redis URL yields a bracketed uri.getHost() like [::1]; the literal compare misses ([::1] vs canonical 0.0.0.1), and resolveInternalRedisIps then calls InetAddress.getAllByName("[::1]"), which throws and is silently skipped — so the IP-overlap path misses it too.
  • A trailing-dot or mixed-case literal host slips through the literal compare for the same reason.

Routing the stored values through normalizeHostForComparisonQuietly keeps both sides symmetric.

♻️ Normalize on store
         try {
             final URI uri = URI.create(url.trim());
             final String host = uri.getHost();
             if (StringUtils.hasText(host)) {
-                hosts.add(host.trim().toLowerCase(Locale.ROOT));
+                hosts.add(normalizeHostForComparisonQuietly(host));
             }
         } catch (IllegalArgumentException e) {
         final Set<String> normalized = new HashSet<>(hosts.length);
         for (String host : hosts) {
             if (StringUtils.hasText(host)) {
-                normalized.add(host.trim().toLowerCase(Locale.ROOT));
+                normalized.add(normalizeHostForComparisonQuietly(host));
             }
         }
🤖 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/RestrictedHostFilter.java`
around lines 165 - 201, The internal Redis host storage methods
(computeInternalRedisHosts, addInternalRedisHostFromEnv, and
setInternalRedisHostsForTesting) normalize hosts using only
trim().toLowerCase(Locale.ROOT), but the comparison methods (isHostBlocked,
isLiteralBlocked) use fully canonicalized normalization via
normalizeHostForComparison*. This mismatch causes IPv6 addresses in brackets and
other edge cases to fail comparisons. Apply normalizeHostForComparisonQuietly to
all host values before storing them in the set across all three methods to
ensure symmetric normalization between storage and comparison.
🤖 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/RestrictedHostFilter.java`:
- Around line 300-336: The isHostBlocked method validates a hostname once and
returns a boolean, but the caller then passes the original hostname string to
JedisPool, which re-resolves it independently, creating a DNS-rebinding window
where a hostile resolver can return different IPs between the two resolutions.
To fix this, modify the isHostBlocked method to return the resolved InetAddress
array instead of just a boolean, then update the caller in RedisPlugin's
assertHostAllowed to use the pre-resolved InetAddress when creating the Jedis
connection pool instead of the original hostname string. This ensures the
validated IP addresses are used and prevents re-resolution by Jedis.

In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/util/RestrictedHostFilterTest.java`:
- Around line 90-109: The tests resolveIfAllowed_allowsLegitimateSmtpHosts,
resolveIfAllowed_returnsResolvedAddress, and other test methods rely on real
public DNS names that require network resolution, making them flaky in
restricted environments. Replace the public SMTP hostnames in the `@ValueSource`
and test method calls with localhost or 127.0.0.1, which are guaranteed to
resolve without external dependencies. Additionally, update the IPv4-only regex
pattern in resolveIfAllowed_returnsResolvedAddress that matches
\d+\.\d+\.\d+\.\d+ to support both IPv4 and IPv6 address formats, or adjust the
assertion to be more flexible for loopback addresses.

In
`@app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java`:
- Around line 541-546: The MockWebServer instance created in the redirect test
is started but never shut down, causing thread and socket leaks. Add a
shutdown() call for the mockWebServer in the finally block where SSRF state is
being reset to ensure proper cleanup. This applies to both redirect test cases
mentioned in the comment (the one at lines 541-546 and the similar one around
lines 614-618).

In
`@app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java`:
- Around line 2012-2018: The test is creating a new MockWebServer instance and
assigning it to the mockEndpoint variable, which overwrites the instance created
in the setUp() `@BeforeEach` method. This causes the original server to leak since
tearDown() only closes the replacement instance. Instead of reassigning
mockEndpoint, create a separate local MockWebServer variable with a different
name (for example, mockRedirectEndpoint) to hold this test-specific redirect
response server, and ensure both servers are properly managed and closed in the
test.
- Around line 1883-1885: The GCP metadata test method that follows this comment
block is not enforcing SSRF filter verification like the other
testDenyInstanceMetadata* tests. Wrap the GCP metadata test body with
setSsrfFilterDisabledForTesting(false) at the beginning to ensure the SSRF
filter is enabled for this test, and then modify the assertion to specifically
check for the "Host not allowed." error message instead of just checking for
generic failure. This ensures the test properly verifies filter blocking
behavior rather than relying on generic network/DNS failures that could mask
filter regressions.

---

Outside diff comments:
In
`@app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java`:
- Around line 262-290: The hostname is validated in `assertHostAllowed()` before
JedisPool creation, but the actual TCP connection occurs later when
`getResource()` is called on the JedisPool instance. An attacker-controlled
hostname could resolve to a different IP address between validation and
connection time. Locate all places where `getResource()` is called on the
JedisPool and add post-connection validation immediately after to verify that
the remote peer address matches the hostname that was validated in
`assertHostAllowed()`, or alternatively document the deliberate
Time-Of-Check-Time-Of-Use acceptance with clear risk justification if you decide
to accept this DNS rebinding risk.

---

Nitpick comments:
In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java`:
- Around line 338-355: The resolveInternalRedisIps method performs DNS lookups
repeatedly on every isHostBlocked call, which is inefficient since Redis hosts
are static. Implement caching for the DNS resolution results by storing the
resolved IPs in a cached field that is computed once during initialization or on
first use, rather than recalculating them on each method invocation. Consider
using a memoization approach or a class-level cache that stores the result of
resolveInternalRedisIps so that subsequent calls to isHostBlocked can reuse the
cached IP set instead of triggering fresh DNS lookups.
- Around line 165-201: The internal Redis host storage methods
(computeInternalRedisHosts, addInternalRedisHostFromEnv, and
setInternalRedisHostsForTesting) normalize hosts using only
trim().toLowerCase(Locale.ROOT), but the comparison methods (isHostBlocked,
isLiteralBlocked) use fully canonicalized normalization via
normalizeHostForComparison*. This mismatch causes IPv6 addresses in brackets and
other edge cases to fail comparisons. Apply normalizeHostForComparisonQuietly to
all host values before storing them in the set across all three methods to
ensure symmetric normalization between storage and comparison.
🪄 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: dfce919e-7c16-4f1c-9527-2d9559075821

📥 Commits

Reviewing files that changed from the base of the PR and between 4a30154 and 23bbc6d.

📒 Files selected for processing (14)
  • app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java
  • app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java
  • app/server/appsmith-interfaces/src/test/java/com/appsmith/external/connections/OAuth2ClientCredentialsTest.java
  • app/server/appsmith-interfaces/src/test/java/com/appsmith/util/RestrictedHostFilterTest.java
  • app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java
  • app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.java
  • app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java
  • app/server/appsmith-plugins/graphqlPlugin/src/test/java/com/external/plugins/GraphQLPluginTest.java
  • app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
  • app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
  • app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/EnvManagerTest.java
  • app/server/pom.xml

wyattwalter and others added 2 commits June 23, 2026 14:36
Six follow-ups from the CodeRabbit review on the CE PR — none change
behavior beyond closing existing edge-case gaps and tightening tests.
Will collapse into the squash-merged commit.

- RestrictedHostFilter: route stored internal-Redis hosts through
  normalizeHostForComparisonQuietly so the storage form matches the
  compare-side canonicalization. Closes an IPv6-bracketed-URL bypass
  (e.g. redis://[::1]:6379 was stored as "[::1]" but lookups compared
  to "0.0.0.1").
- RedisPlugin.assertHostAllowed: document the deliberate TOCTOU
  acceptance between isHostBlocked and Jedis's own re-resolution.
- RestApiPluginTest.testDenyInstanceMetadataGcp: wrap in
  setSsrfFilterDisabledForTesting(false) and assert "Host not allowed."
  so a network failure to metadata.google.internal can't mask a filter
  regression. Was the one outlier left unwrapped when the AWS variants
  were strengthened.
- RestApiPluginTest.testDenyInstanceMetadataAwsWithRedirect: use a local
  MockWebServer rather than reassigning the @beforeeach `mockEndpoint`
  field; the reassignment leaked the original (tearDown only shuts down
  whatever's in the field at AfterEach time).
- ElasticSearchPluginTest.itShouldRejectGetTo*AndRedirect: add
  mockWebServer.shutdown() in finally so the per-test MockWebServer
  instances don't leak threads / sockets across the suite.
- RestrictedHostFilterTest.resolveIfAllowed_returnsResolvedAddress:
  use the literal IP 1.1.1.1 and assert exact equality, instead of
  smtp.gmail.com + an IPv4-only regex that would fail on
  IPv6-preferred CI environments.
Pure dependency bump, no code changes. Jedis 3.3.0 (2020) predates the
JedisSocketFactory injection API needed to close the DNS-rebinding TOCTOU
in the Redis SSRF host filter (next commit). 5.2.0 keeps the Java 8
baseline and every API the plugin uses is unchanged: JedisPool,
getResource, sendCommand, ping, JedisPoolConfig, Protocol.Command,
SafeEncoder, JedisException all compile as-is (verified locally).

Jedis is used only by redisPlugin; the server's own Redis state runs on
Lettuce, so this bump is plugin-isolated (PF4J classloader + shaded jar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28055924025.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

@github-actions

Copy link
Copy Markdown

Failed server tests

  • com.external.plugins.MssqlPluginTest#
  • com.external.plugins.MssqlGetDBSchemaTest#
  • com.external.plugins.FirestorePluginTest#
  • com.external.plugins.RedshiftPluginTest#
  • com.external.plugins.AmazonS3PluginTest#
  • com.external.utils.AmazonS3ErrorUtilsTest#
  • com.external.config.FileInfoMethodTest#
  • com.external.config.RowsBulkAppendMethodTest#
  • com.external.config.GetDatasourceMetadataMethodTest#
  • com.external.config.MethodConfigTest#
  • com.external.config.RowsBulkUpdateMethodTest#
  • com.external.config.SheetsUtilTest#
  • com.external.config.RowsGetMethodTest#
  • com.external.config.RowsUpdateMethodTest#
  • com.external.config.GetStructureMethodTest#
  • com.external.config.RowsAppendMethodTest#
  • com.external.config.FileListMethodTest#
  • com.external.plugins.GraphQLPluginTest#
  • com.external.plugins.SnowflakePluginTest#
  • com.external.plugins.SmtpPluginTest#
  • com.external.plugins.EmbeddingCommandTest#
  • com.external.plugins.OpenAIPluginTest#
  • com.external.plugins.ChatCommandTest#
  • com.external.plugins.VisionCommandTest#
  • com.external.plugins.AnthropicPluginTest#
  • com.external.plugins.GenerateContentCommandTest#
  • com.external.plugins.GoogleAiPluginTest#
  • com.external.plugins.services.AiFeatureServiceFactoryTest#
  • com.external.plugins.services.HeadersUtilTest#
  • com.external.plugins.services.features.ImageCaptioningServiceImplTest#
  • com.external.plugins.services.features.TextSummarizationServiceImplTest#
  • com.external.plugins.services.features.TextGenerationServiceImplTest#
  • com.external.plugins.services.features.ImageClassificationServiceImplTest#
  • com.external.plugins.services.features.ImageEntityExtractionServiceImplTest#
  • com.external.plugins.services.features.TextClassificationServiceImplTest#
  • com.external.plugins.services.features.TextEntityExtractionServiceImplTest#
  • com.external.plugins.services.FieldValidationHelperTest#
  • com.external.plugins.services.FileUtilTest#
  • com.external.plugins.AwsLambdaPluginTest#
  • com.external.plugins.DatabricksPluginTest#
  • com.appsmith.git.converters.GsonDoubleToLongConverterTest#
  • com.appsmith.git.helpers.DSLTransformerHelperTest#
  • com.appsmith.git.helpers.FileUtilsImplTest#
  • com.appsmith.git.service.BashServiceTest#
  • GsonUnorderedToOrderedSerializationTest#
  • com.external.plugins.RedisPluginTest#itShouldThrowErrorIfHostnameIsInvalid

wyattwalter and others added 2 commits June 23, 2026 16:07
The Jedis 5.x client reports an unresolvable host with a different
exception message than 3.x did, so the assertion pinned to
"Failed connecting to <host>:<port>" no longer matches. Assert the
behavioral contract instead — an invalid host yields an unsuccessful
DatasourceTestResult carrying a non-empty error — which is robust across
driver versions. Necessitated by the Jedis 3.3.0 to 5.2.0 upgrade.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lution socket factory

The create-time pre-check (RestrictedHostFilter.isHostBlocked) and Jedis's
own connect-time resolution were two independent DNS lookups — a hostile
resolver could return an allowed IP to the pre-check and the internal Redis
IP to the driver (DNS rebinding). Now requires Jedis 5.2.0's socket-factory
injection (prior commits).

- Add RestrictedHostFilter.firstAllowedRedisAddress(host, resolved): pure
  policy check over an already-resolved address set, returning the validated
  address to connect to (or empty if blocked). Caller owns the single DNS
  resolution.
- Add RestrictedHostJedisSocketFactory: resolves once at connect time,
  validates via the above, and connects the socket directly to the pinned
  IP so the driver never re-resolves. For rediss:// the plaintext socket
  connects to the pinned IP while the original hostname is used for SNI /
  cert verification; connect + TLS logic mirrors DefaultJedisSocketFactory.
- Wire it into RedisPlugin.datasourceCreate via
  JedisPool(poolConfig, socketFactory, clientConfig), deriving the client
  config from the URI exactly as Jedis's own URI constructor does. The
  isHostBlocked pre-check stays as create-time UX/defense-in-depth.
- Test that the factory rejects a denylisted host at connect time.

See GHSA-qhfj-g87x-m39w.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28057260205.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

@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: 1

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

384-414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deny-logic is a near-duplicate of isHostBlocked — extract a shared helper to avoid SSRF drift.

The block-checking core here (canonical denylist + isBlockedIpAddressClass, internal-Redis hostname match, then the per-address resolveInternalRedisIps/matchesBlockedAddressClass loop) is copy-pasted from isHostBlocked (Lines 311-339). In security-sensitive code that's a maintenance hazard: a future tweak to one path can silently leave the other open. The method itself is correct and fail-closed — this is purely about keeping the two gates in lockstep.

♻️ Extract a shared resolved-set check
+    private static boolean isResolvedSetBlocked(String canonicalHost, InetAddress[] addresses) {
+        if (DISALLOWED_HOSTS.contains(canonicalHost) || isBlockedIpAddressClass(canonicalHost)) {
+            return true;
+        }
+        final Set<String> redisHosts = internalRedisHosts;
+        if (redisHosts.contains(canonicalHost)) {
+            return true;
+        }
+        final Set<String> internalRedisIps = resolveInternalRedisIps(redisHosts);
+        for (InetAddress addr : addresses) {
+            final String addrString = normalizeHostForComparisonQuietly(addr.getHostAddress());
+            if (DISALLOWED_HOSTS.contains(addrString)
+                    || matchesBlockedAddressClass(addr)
+                    || internalRedisIps.contains(addrString)) {
+                return true;
+            }
+        }
+        return false;
+    }
         if (ssrfFilterDisabled || isExplicitlyAllowedForTesting(host)) {
             return Optional.of(resolvedAddresses[0]);
         }
-
         final String canonicalHost = normalizeHostForComparisonQuietly(host);
-        if (DISALLOWED_HOSTS.contains(canonicalHost) || isBlockedIpAddressClass(canonicalHost)) {
-            return Optional.empty();
-        }
-
-        final Set<String> redisHosts = internalRedisHosts;
-        if (redisHosts.contains(canonicalHost)) {
-            return Optional.empty();
-        }
-
-        final Set<String> internalRedisIps = resolveInternalRedisIps(redisHosts);
-        for (InetAddress addr : resolvedAddresses) {
-            final String addrString = normalizeHostForComparisonQuietly(addr.getHostAddress());
-            if (DISALLOWED_HOSTS.contains(addrString)
-                    || matchesBlockedAddressClass(addr)
-                    || internalRedisIps.contains(addrString)) {
-                return Optional.empty();
-            }
-        }
-
-        return Optional.of(resolvedAddresses[0]);
+        return isResolvedSetBlocked(canonicalHost, resolvedAddresses)
+                ? Optional.empty()
+                : Optional.of(resolvedAddresses[0]);

isHostBlocked can then resolve and delegate to the same helper.

🤖 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/RestrictedHostFilter.java`
around lines 384 - 414, Extract the common deny-logic that is duplicated between
firstAllowedRedisAddress and isHostBlocked into a shared helper method. The
helper should encapsulate the core checks: canonical host validation against
DISALLOWED_HOSTS, IP address class validation using isBlockedIpAddressClass,
internal Redis hostname matching against internalRedisHosts, and the per-address
loop that checks each InetAddress against DISALLOWED_HOSTS,
matchesBlockedAddressClass, and resolveInternalRedisIps. Replace the duplicate
logic blocks in both firstAllowedRedisAddress and isHostBlocked to call this
shared helper, ensuring both methods use identical deny-list evaluation logic to
prevent SSRF security gaps.
🤖 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-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java`:
- Around line 289-298: The DefaultJedisClientConfig.builder() chain that
constructs clientConfig is missing the hostnameVerifier configuration, which
disables TLS hostname verification for rediss:// datasources. Add a
.hostnameVerifier() method call to the builder chain (before the .build() call)
to enable proper TLS certificate hostname verification and prevent attacks using
mismatched certificates on pinned IPs.

---

Nitpick comments:
In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java`:
- Around line 384-414: Extract the common deny-logic that is duplicated between
firstAllowedRedisAddress and isHostBlocked into a shared helper method. The
helper should encapsulate the core checks: canonical host validation against
DISALLOWED_HOSTS, IP address class validation using isBlockedIpAddressClass,
internal Redis hostname matching against internalRedisHosts, and the per-address
loop that checks each InetAddress against DISALLOWED_HOSTS,
matchesBlockedAddressClass, and resolveInternalRedisIps. Replace the duplicate
logic blocks in both firstAllowedRedisAddress and isHostBlocked to call this
shared helper, ensuring both methods use identical deny-list evaluation logic to
prevent SSRF security gaps.
🪄 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: 7cc48cd9-b20d-4f66-b68d-58a11a351800

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5ffa0 and c046f6b.

📒 Files selected for processing (4)
  • app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java
  • app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java
  • app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RestrictedHostJedisSocketFactory.java
  • app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java

Comment on lines +289 to +298
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.connectionTimeoutMillis(timeout)
.socketTimeoutMillis(timeout)
.user(JedisURIHelper.getUser(uri))
.password(JedisURIHelper.getPassword(uri))
.database(JedisURIHelper.getDBIndex(uri))
.ssl(JedisURIHelper.isRedisSSLScheme(uri))
.build();
JedisSocketFactory socketFactory =
new RestrictedHostJedisSocketFactory(hostAndPort, clientConfig);

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 | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Jedis JedisPool(poolConfig, URI, timeout) perform TLS hostname verification by default for rediss scheme

💡 Result:

Yes, Jedis now performs TLS hostname verification by default for rediss:// schemes, provided you are using Jedis version 7.5.0 or later [1][2]. In versions prior to 7.5.0, TLS hostname verification was not enforced by default [3][1]. Starting with Jedis 7.5.0, the client was updated to enforce hostname verification automatically when SSL is enabled (which occurs when using the rediss scheme or explicitly setting ssl(true)) [1][2]. Specifically, the library now sets the endpoint identification algorithm to HTTPS (SSLParameters.setEndpointIdentificationAlgorithm("HTTPS")) by default, ensuring that the certificate's Common Name (CN) or Subject Alternative Name (SAN) matches the host being connected to [4][2]. If this verification fails, the connection attempt will result in an SSL handshake error [4][1]. If you need to bypass this verification (e.g., for legacy environments where certificate hostnames do not match the connection endpoints), you can disable it by explicitly providing custom SSLParameters with a null or empty endpoint identification algorithm [4][2]. Additionally, newer versions of Jedis (as of the 7.x series) encourage the use of the SslOptions API for more explicit configuration of TLS verification modes [4][5][1].

Citations:


TLS cert hostname verification is effectively off.

clientConfig omits hostnameVerifier, so for rediss:// datasources the verification check at line 106 of RestrictedHostJedisSocketFactory never executes. Since Jedis 5.2.0 (in use here) does not enforce TLS hostname verification by default, this means an attacker presenting any valid-but-mismatched certificate to the pinned IP would not be caught.

This is not a regression from the pre-PR URI-based JedisPool constructor, which also lacked hostname verification with Jedis 5.2.0. However, if hostname verification should be enforced, add .hostnameVerifier() to the DefaultJedisClientConfig.builder().

🤖 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/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java`
around lines 289 - 298, The DefaultJedisClientConfig.builder() chain that
constructs clientConfig is missing the hostnameVerifier configuration, which
disables TLS hostname verification for rediss:// datasources. Add a
.hostnameVerifier() method call to the builder chain (before the .build() call)
to enable proper TLS certificate hostname verification and prevent attacks using
mismatched certificates on pinned IPs.

…dRedisAddress

Both methods evaluated the deny list independently (canonical denylist +
IP-class check + internal-Redis literal, then a per-resolved-address loop).
Extract isCanonicalHostBlocked and isAnyResolvedAddressBlocked so the two
share one evaluation and cannot drift into an SSRF gap. Behavior-preserving:
isHostBlocked keeps its canonical short-circuit before DNS and its
unresolvable-host-returns-false contract. RestrictedHostFilterTest (87) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28060123190.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

…datasources

Jedis 5.2.0 does not verify the certificate hostname by default (only the
trust chain), so a MITM presenting any CA-trusted-but-mismatched cert on the
pinned IP would be accepted. Set the endpoint identification algorithm to
HTTPS in the socket factory so the cert SAN/CN is checked against the
configured hostname (we connect to the pinned IP but pass the hostname to the
TLS layer, so verification runs against the hostname). Defaults to HTTPS only
when a caller hasn't set its own endpoint-identification policy.

Low blast radius: rediss:// support was added 2026-03-03 (#41587) and is
barely adopted. Chain validation was already on, so self-signed certs already
failed; this newly rejects only CA-trusted-but-mismatched certs and
IP-addressed rediss:// (certs rarely carry IP SANs). No opt-out env var yet —
add one if a real datasource needs a mismatched/IP cert.

Addresses the CodeRabbit "TLS hostname verification effectively off" finding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28072374766.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

…n instead of a generic error

testDatasource read error.getCause().getMessage(), but the socket factory's
connection-time rejection throws a causeless JedisConnectionException
(message "Host not allowed."), so getCause() was null and the call NPE'd —
the user saw a generic "Couldn't establish a connection" instead of the
intended message. This is the DNS-rebinding path: the host passes the
create-time pre-check (resolves to an allowed IP) but the socket factory's
own resolution lands on a blocked IP and refuses. Fall back to the error's
own message when there's no cause (also fixes the latent NPE on the no-PONG
path). Add a log line in the factory so the rejection is visible server-side.

Verified on the deploy preview with rbndr.us flipping 172.31.4.163 <-> 127.0.0.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28108451594.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

The connect-time rejection logs (RestrictedHostJedisSocketFactory), but the
create-time pre-check (assertHostAllowed) — which catches the common case of a
datasource pointed at a disallowed host — threw silently. Log it at WARN; the
log framework tags the line with the requesting userEmail / orgId / traceId, so
a cluster of these is a useful reconnaissance signal. No server-side trace
otherwise, since the block is returned as a graceful HTTP 200 DatasourceTestResult.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28113194173.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com


private static Set<String> computeInternalRedisHosts() {
final Set<String> hosts = new HashSet<>();
addInternalRedisHostFromEnv(hosts, "APPSMITH_REDIS_URL");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What if Redis is configured using application.properties e.g. appsmith.redis.url. In that case it remains fail-open. Please correct me if I am wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Excellent point. I think this is addressed now.

* connected to. Honors the {@code APPSMITH_DISABLE_SSRF_FILTER} kill-switch and the test
* allowlist, in which case the first resolved address is returned without policy checks.
*/
public static Optional<InetAddress> firstAllowedRedisAddress(String host, InetAddress[] resolvedAddresses) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QQ: Do we need to loop through all the addresses? Could it be possible if the first resolved IP is IPV6 but the actual redis is listening on IPV4 address?

@wyattwalter wyattwalter Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, adjusted that. Good catch.

…ved URL, not just the env var

RestrictedHostFilter built its internal-Redis set only from the
APPSMITH_REDIS_URL / APPSMITH_REDIS_GIT_URL environment variables at static
init. But the app binds appsmith.redis.url via Spring, which can also resolve
from application.properties, a -D system property, or a profile. An operator
configuring Redis that way (no env var) left the internal-Redis set empty, so a
datasource could be pointed at the in-cluster Redis — fail-open. (Loopback and
cloud-metadata literals were still blocked, but an RFC1918 address or a service
hostname was not.)

RedisConfig now registers the resolved appsmith.redis.url / appsmith.redis.git.url
with the filter at startup (@PostConstruct, before any datasource test), unioning
with the env-derived seed so configuring through either channel blocks both. The
filter is shared with the redisPlugin (appsmith-interfaces is provided-scope to
plugins, so it's parent-loaded), so the pushed state is visible at the connection
path. RestrictedHostFilterTest covers the new registration and union semantics.

See GHSA-qhfj-g87x-m39w.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28193528930.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

…st the first

RestrictedHostJedisSocketFactory connected to only the first resolved address.
For a dual-stack host whose leading record is an unreachable IPv6 (AAAA first,
but Redis listens on IPv4 / the client has no IPv6 route), that dropped the
connection even though a reachable IPv4 was in the resolved set — a resilience
regression versus stock DefaultJedisSocketFactory, which tries each address in
turn. Now iterate the resolved addresses and connect to the first reachable one.

Every address was already validated by the single firstAllowedRedisAddress()
call (present only if none is blocked), so failover does not widen the SSRF
surface and the single-resolution guarantee is unchanged. Raised in PR review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28194282012.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

Both block log lines recorded only the host, so a hostname that resolved to
loopback / cloud-metadata looked identical to one typed as an internal IP
literally. Add the resolved address(es) for forensics:

- Connection-time (RestrictedHostJedisSocketFactory) logs its own already-
  validated resolution — exact, no extra lookup.
- Create-time pre-check (RedisPlugin) uses a new log-only helper,
  RestrictedHostFilter.describeResolvedAddresses, which does a best-effort
  lookup ("unresolved" when it can't resolve). It is point-in-time and not a
  security decision, since isHostBlocked only returns a boolean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wyattwalter

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28199055848.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41921.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41921.dp.appsmith.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants