Skip to content

fix(security): block full loopback range in SSRF protection#41694

Open
subrata71 wants to merge 3 commits intoreleasefrom
subrata71/fix-loopback-bypass
Open

fix(security): block full loopback range in SSRF protection#41694
subrata71 wants to merge 3 commits intoreleasefrom
subrata71/fix-loopback-bypass

Conversation

@subrata71
Copy link
Copy Markdown
Collaborator

@subrata71 subrata71 commented Apr 3, 2026

Description

The SSRF protection in WebClientUtils only blocked 127.0.0.1 via exact string match in the DISALLOWED_HOSTS set. On Linux, the entire 127.0.0.0/8 subnet routes to localhost, so addresses like 127.0.0.2 bypassed the filter and could reach internal services bound to 127.0.0.1 (e.g., supervisord XML-RPC on port 9001).

Root cause

  • requestFilterFn (used by REST API plugin) and NameResolver only checked against DISALLOWED_HOSTS — a string set containing only 127.0.0.1
  • resolveIfAllowed (used for SMTP) already used InetAddress.isLoopbackAddress() which covers the full /8 range, but the HTTP path did not

Fix

  • Added isBlockedByAddressType(InetAddress) helper that checks isLoopbackAddress(), isLinkLocalAddress(), isAnyLocalAddress(), isMulticastAddress(), and IPv6 ULA (fc00::/7)
  • Integrated into requestFilterFn (pre-request filter), isDisallowedAndFail (DNS resolver), and resolveIfAllowed (SMTP path) — defense in depth at all three layers
  • Refactored resolveIfAllowed to reuse the shared helper, eliminating code duplication

Tests

  • testRequestFilterFnRejectsFullLoopbackRange — verifies 127.0.0.2, 127.0.1.1, 127.255.255.254, 0.0.0.0 are rejected at the request filter level
  • testIsDisallowedAndFailBlocksFullLoopbackRange — verifies the DNS resolver layer blocks the full range
  • resolveIfAllowed_blocksFullLoopbackRange — verifies the SMTP path continues to block the full range

Fixes GHSA RCE via Supervisord XML-RPC — Incomplete Loopback Blocklist Allows 127.0.0.2 Bypass

Automation

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

🔍 Cypress test results

Tip

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


Tue, 07 Apr 2026 19:23:17 UTC

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • No

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Request filtering now rejects addresses based on runtime address-type checks (e.g., loopback and any-local), ensuring full IPv4 loopback range (127.0.0.0/8) and 0.0.0.0 are blocked consistently across outbound requests.
  • Tests

    • Added parameterized tests validating complete loopback-range rejection and address-type based blocking behavior.

…tion

The SSRF protection in WebClientUtils only blocked 127.0.0.1 by exact
string match, allowing addresses like 127.0.0.2 to bypass the filter
and reach internal services (e.g. supervisord on port 9001).

Add isBlockedByAddressType() helper that uses InetAddress type checks
(isLoopbackAddress, isLinkLocalAddress, isAnyLocalAddress, isMulticastAddress,
IPv6 ULA) and integrate it into requestFilterFn, isDisallowedAndFail, and
resolveIfAllowed. This provides defense-in-depth at all three SSRF check
layers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Walkthrough

Introduces a mutable static IN_DOCKER flag and centralizes address-type blocking in WebClientUtils via isBlockedByAddressType and isHostDisallowed. Request filtering and name resolution now use runtime address-type checks (loopback always blocked in the resolve path) in addition to the static denylist. Tests were extended to cover full IPv4 loopback range and manipulate IN_DOCKER during test lifecycle.

Changes

Cohort / File(s) Summary
Core blocking logic
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java
Added mutable static IN_DOCKER; extracted address-type rules into isBlockedByAddressType(InetAddress); added isHostDisallowed(String) to combine denylist and runtime IP checks; switched resolveIfAllowed() and request filter to use these helpers (loopback now always blocked in resolve path).
Tests: lifecycle & loopback coverage
app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java
Uses reflection to save/override/restore IN_DOCKER in @BeforeAll/@AfterAll; added parameterized tests asserting request filter, isDisallowedAndFail, and resolveIfAllowed block full IPv4 loopback examples (e.g., 127.0.0.2, 127.0.1.1, 127.255.255.254, 0.0.0.0).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🔒 A tiny flag flipped inside the dock,
New guards on addresses stand and block,
Helpers tidy rules once spread and wild,
Loopback's boxed where tests were compiled,
CI hums—network gates now locked. 🚪

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main security fix: blocking the full loopback range in SSRF protection rather than just the single address.
Description check ✅ Passed The PR description is well-structured, addresses the root cause, details the fix implementation across three layers, documents tests, and includes security context.

✏️ 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 subrata71/fix-loopback-bypass

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 and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 3, 2026

Failed server tests

  • com.external.plugins.AmazonS3PluginTest#
  • com.external.utils.AmazonS3ErrorUtilsTest#
  • com.external.plugins.ChatCommandTest#
  • com.external.plugins.AnthropicPluginTest#
  • com.external.plugins.services.AiFeatureServiceFactoryTest#
  • com.external.plugins.services.FieldValidationHelperTest#
  • com.external.plugins.services.FileUtilTest#
  • com.external.plugins.services.HeadersUtilTest#
  • com.external.plugins.services.features.ImageClassificationServiceImplTest#
  • com.external.plugins.services.features.TextGenerationServiceImplTest#
  • com.external.plugins.services.features.TextSummarizationServiceImplTest#
  • com.external.plugins.services.features.ImageCaptioningServiceImplTest#
  • com.external.plugins.services.features.TextEntityExtractionServiceImplTest#
  • com.external.plugins.services.features.TextClassificationServiceImplTest#
  • com.external.plugins.services.features.ImageEntityExtractionServiceImplTest#
  • com.external.plugins.ArangoDBPluginTest#
  • com.external.utils.StructureUtilsTest#
  • com.external.plugins.AwsLambdaPluginTest#
  • com.external.plugins.DatabricksPluginTest#
  • com.external.plugins.DynamoPluginTest#
  • com.external.plugins.ElasticSearchPluginTest#
  • com.external.plugins.FirestorePluginTest#
  • com.external.plugins.GoogleAiPluginTest#
  • com.external.plugins.GenerateContentCommandTest#
  • com.external.config.RowsUpdateMethodTest#
  • com.external.config.GetDatasourceMetadataMethodTest#
  • com.external.config.FileListMethodTest#
  • com.external.config.FileInfoMethodTest#
  • com.external.config.MethodConfigTest#
  • com.external.config.SheetsUtilTest#
  • com.external.config.RowsGetMethodTest#
  • com.external.config.RowsAppendMethodTest#
  • com.external.config.GetStructureMethodTest#
  • com.external.config.RowsBulkUpdateMethodTest#
  • com.external.config.RowsBulkAppendMethodTest#
  • com.external.plugins.GraphQLPluginTest#
  • com.external.plugins.MongoPluginFormsTest#
  • com.external.plugins.MongoPluginStaleConnTest#
  • com.external.plugins.MongoPluginQueriesTest#
  • com.external.plugins.MongoPluginDatasourceTest#
  • com.external.plugins.MongoPluginDataTypeTest#
  • com.external.plugins.utils.MongoPluginUtilsTest#
  • com.external.plugins.utils.DatasourceUtilsTest#
  • com.external.plugins.MongoPluginErrorsTest#
  • com.external.plugins.MongoPluginRegexTest#
  • com.external.plugins.MssqlGetDBSchemaTest#
  • com.external.plugins.MssqlPluginTest#
  • com.external.plugins.MySqlPluginTest#
  • com.external.plugins.MySQLPluginDataTypeTest#
  • com.external.plugins.MySqlStaleConnectionErrorMessageTest#
  • com.external.plugins.MySQLDatasourceValidationTest#
  • com.external.utils.QueryUtilsTest#
  • com.external.plugins.OpenAIPluginTest#
  • com.external.plugins.VisionCommandTest#
  • com.external.plugins.EmbeddingCommandTest#
  • com.external.plugins.OraclePluginDatasourceValidityErrorsTest#
  • com.external.plugins.OracleConnectionRateLimitTest#
  • com.external.plugins.OraclePluginErrorsTest#
  • com.external.plugins.OraclePluginConnectionTest#
  • com.external.plugins.OracleExecutionTest#
  • com.external.plugins.OracleGetDBSchemaTest#
  • com.external.plugins.PostgresPluginDataTypeTest#
  • com.external.plugins.PostgresPluginTest#
  • com.external.plugins.PostgresDatasourceValidationTest#
  • com.external.plugins.RedisPluginTest#
  • com.external.utils.RedisURIUtilsTest#
  • com.external.plugins.RedshiftPluginTest#
  • com.external.plugins.RestApiPluginTest#
  • com.external.plugins.SmtpPluginTest#
  • com.external.plugins.SnowflakePluginTest#
  • GsonUnorderedToOrderedSerializationTest#
  • com.appsmith.git.service.BashServiceTest#
  • com.appsmith.git.converters.GsonDoubleToLongConverterTest#
  • com.appsmith.git.helpers.FileUtilsImplTest#
  • com.appsmith.git.helpers.DSLTransformerHelperTest#
  • com.appsmith.external.connections.OAuth2ClientCredentialsTest#testCreate_withIsAuthorizationHeaderFalse_sendsCredentialsInBody
  • com.appsmith.external.connections.OAuth2ClientCredentialsTest#testCreate_withIsAuthorizationHeaderTrue_sendsCredentialsInHeader

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java (2)

101-111: Consider adding 0.0.0.0 for consistency.

The requestFilterFn test includes 0.0.0.0, but this test doesn't. While 0.0.0.0 is tested elsewhere (line 57), adding it here would make the coverage symmetric across all three SSRF layers.

Suggested addition
 `@ValueSource`(
         strings = {
             "127.0.0.2",
             "127.0.1.1",
             "127.255.255.254",
+            "0.0.0.0",
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java`
around lines 101 - 111, Add 0.0.0.0 to the parameterized inputs for the test
method testIsDisallowedAndFailBlocksFullLoopbackRange so the
WebClientUtils.isDisallowedAndFail coverage matches other SSRF tests; update the
`@ValueSource` strings in that test to include "0.0.0.0" alongside the existing
loopback addresses to ensure symmetry with requestFilterFn and the earlier test
that already covers 0.0.0.0.

84-123: Consider adding an explicit IPv6 loopback test for completeness.

IPv6 loopback (::1) is implicitly covered by InetAddress.isLoopbackAddress() in isBlockedByAddressType(), which blocks all loopback addresses regardless of the DISALLOWED_HOSTS list. However, adding an explicit test with ::1 would match the thoroughness of the IPv4 loopback coverage and provide more immediate verification of the IPv6 path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java`
around lines 84 - 123, Add explicit IPv6 loopback ("::1") test cases to the
WebClientUtilsTest suite: create tests that call
invokeRequestFilterFn("http://[::1]:9001/RPC2") and assert it yields
UnknownHostException with message WebClientUtils.HOST_NOT_ALLOWED, call
WebClientUtils.isDisallowedAndFail("::1", null) and assert true, and call
WebClientUtils.resolveIfAllowed("::1") and assert the result is empty; place
these alongside the existing IPv4 loopback tests so the IPv6 loopback path in
WebClientUtils.isBlockedByAddressType() is explicitly validated.
app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java (1)

313-326: Minor inefficiency: host is normalized twice.

canonicalHost is already normalized at line 315, but isHostDisallowed normalizes it again internally. Not a bug since normalization is idempotent, but slightly wasteful.

Consider either:

  1. Passing raw host to isHostDisallowed (preferred - simpler call site), or
  2. Creating an overload that accepts pre-normalized input

Low priority given this is a security fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java`
around lines 313 - 326, The code normalizes host into canonicalHost via
normalizeHostForComparison but then calls isHostDisallowed(canonicalHost) which
causes a second normalization inside isHostDisallowed; to avoid the duplicate
work, call isHostDisallowed(host) from the WebClientUtils method (keeping the
existing internal normalization in isHostDisallowed) or alternatively add an
overload like isHostDisallowedNormalized(String canonicalHost) that assumes
already-normalized input and call that here; update the call site around
canonicalHost and remove the redundant normalization usage accordingly while
keeping the existing error handling around normalizeHostForComparison.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java`:
- Around line 313-326: The code normalizes host into canonicalHost via
normalizeHostForComparison but then calls isHostDisallowed(canonicalHost) which
causes a second normalization inside isHostDisallowed; to avoid the duplicate
work, call isHostDisallowed(host) from the WebClientUtils method (keeping the
existing internal normalization in isHostDisallowed) or alternatively add an
overload like isHostDisallowedNormalized(String canonicalHost) that assumes
already-normalized input and call that here; update the call site around
canonicalHost and remove the redundant normalization usage accordingly while
keeping the existing error handling around normalizeHostForComparison.

In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java`:
- Around line 101-111: Add 0.0.0.0 to the parameterized inputs for the test
method testIsDisallowedAndFailBlocksFullLoopbackRange so the
WebClientUtils.isDisallowedAndFail coverage matches other SSRF tests; update the
`@ValueSource` strings in that test to include "0.0.0.0" alongside the existing
loopback addresses to ensure symmetry with requestFilterFn and the earlier test
that already covers 0.0.0.0.
- Around line 84-123: Add explicit IPv6 loopback ("::1") test cases to the
WebClientUtilsTest suite: create tests that call
invokeRequestFilterFn("http://[::1]:9001/RPC2") and assert it yields
UnknownHostException with message WebClientUtils.HOST_NOT_ALLOWED, call
WebClientUtils.isDisallowedAndFail("::1", null) and assert true, and call
WebClientUtils.resolveIfAllowed("::1") and assert the result is empty; place
these alongside the existing IPv4 loopback tests so the IPv6 loopback path in
WebClientUtils.isBlockedByAddressType() is explicitly validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 97386ccf-aa9e-4ead-aea0-d2889e6afa50

📥 Commits

Reviewing files that changed from the base of the PR and between a8a43b2 and 8411e43.

📒 Files selected for processing (2)
  • app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java
  • app/server/appsmith-interfaces/src/test/java/com/appsmith/util/WebClientUtilsTest.java

The isBlockedByAddressType check was blocking loopback addresses
unconditionally, but loopback should only be blocked inside Docker
(matching the existing DISALLOWED_HOSTS policy). This broke
OAuth2ClientCredentialsTest which uses MockWebServer on localhost.

- Extract IN_DOCKER into a static field, gate loopback check on it
- resolveIfAllowed (SMTP path) always blocks loopback regardless
- Tests enable Docker mode via reflection to verify loopback blocking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@subrata71 subrata71 self-assigned this Apr 4, 2026
@subrata71 subrata71 added the ok-to-test Required label for CI label Apr 4, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant