fix(security): block SSRF via JDBC hostname validation in all database plugins (GHSA-jq9r-32xv-wgpp)#41688
fix(security): block SSRF via JDBC hostname validation in all database plugins (GHSA-jq9r-32xv-wgpp)#41688
Conversation
…e plugins (GHSA-jq9r-32xv-wgpp) The Snowflake plugin concatenated user input directly into JDBC URLs without validation, allowing '#' fragment injection to redirect connections to arbitrary hosts. Other JDBC plugins lacked expanded character validation. This adds a shared JdbcHostValidator utility that blocks URI-special characters (#, /, :, @, ?, \) in hostnames across all 6 JDBC plugins (Snowflake, PostgreSQL, MySQL, MSSQL, Oracle, Redshift). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WalkthroughCentralized JDBC hostname validation and SSRF checks added via a new Changes
Sequence DiagramsequenceDiagram
participant Plugin as Plugin Executor
participant Validator as JdbcHostValidator
participant WebClient as WebClientUtils
participant Invalids as Validation Result
Plugin->>Validator: validateHostname(host)
alt host null or blank
Validator-->>Plugin: Optional with "Missing hostname" error
Plugin->>Invalids: add error
else host contains disallowed chars
Validator-->>Plugin: Optional with "Host value cannot contain..." error
Plugin->>Invalids: add error
else valid format
Validator-->>Plugin: Optional.empty
end
Plugin->>Validator: checkSsrfProtection(host)
alt host null or blank
Validator-->>Plugin: Optional with SSRF error
Plugin->>Invalids: add error
else non-empty
Validator->>WebClient: resolveIfAllowed(host)
alt resolution blocked
WebClient-->>Validator: Optional.empty
Validator-->>Plugin: Optional with "blocked" error
Plugin->>Invalids: add error
else resolution allowed
WebClient-->>Validator: Optional.of(resolution)
Validator-->>Plugin: Optional.empty
end
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.java (1)
184-189: Strengthen the new hash-hostname assertion with input echo verification.
The current check only validates a generic fragment. Including the tested host value would better ensure the error is tied to this exact invalid input.Suggested test tweak
- assertTrue(output.stream().anyMatch(msg -> msg.contains("Host value cannot contain"))); + assertTrue(output.stream() + .anyMatch(msg -> msg.contains("Host value cannot contain") && msg.contains("evil.com#fragment")));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.java` around lines 184 - 189, Update the test testValidateDatasource_withHashInHostname_returnsInvalid to assert the validation message includes the exact invalid host value; after setting dsConfig.getEndpoints().get(0).setHost("evil.com#fragment"), ensure the assertion on pluginExecutor.validateDatasource(dsConfig) checks both the generic message ("Host value cannot contain") and that the message echoes the host string ("evil.com#fragment") so the error is tied to this specific input.app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/JdbcHostValidatorTest.java (1)
14-14: Optional: Consider adding IPv6 test cases.The current coverage is solid for IPv4 and hostnames. If the validator is expected to handle IPv6 addresses (e.g.,
::1,fe80::1), adding explicit test cases would improve confidence.Also applies to: 42-42
🤖 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/external/helpers/JdbcHostValidatorTest.java` at line 14, Add IPv6 cases to the parameterized test in JdbcHostValidatorTest by including representative IPv6 literals (e.g., "::1", "fe80::1", and a full IPv6 like "2001:0db8:85a3::8a2e:0370:7334") in the `@ValueSource` strings so the validator is exercised for IPv6 input; update the `@ValueSource` on the test method in JdbcHostValidatorTest (the parameterized test annotated with `@ValueSource`(strings = {...})) to include these entries and ensure any expected assertions in the same test method still hold for IPv6 addresses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java`:
- Around line 400-406: The validation loop in MssqlPlugin that iterates
datasourceConfiguration.getEndpoints() currently only calls
JdbcHostValidator.validateHostname(...) and thus misses SSRF checks; update the
loop so that for each endpoint you also invoke
JdbcHostValidator.checkSsrfProtection(endpoint.getHost()) and, if it returns a
present value, add that message to the invalids list (same as for
validateHostname). Ensure you call checkSsrfProtection before or after
validateHostname for each Endpoint and add its Optional result to invalids::add
so SSRF-blacklisted hosts like 169.254.169.254 are rejected.
In
`@app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java`:
- Around line 780-828: Add SSRF regression tests in MssqlPluginTest that call
mssqlPluginExecutor.validateDatasource with endpoints using loopback and
metadata hosts (e.g., "127.0.0.1" and "169.254.169.254") and assert the
validation output contains the plugin’s "not allowed" / forbidden-host error
path; specifically, add new `@ParameterizedTest` or `@Test` methods mirroring the
existing hostname tests (reuse Endpoint/DatasourceConfiguration/DBAuth setup)
and assert the returned Set<String> from validateDatasource(...) contains the
expected "not allowed" message (or the canonical error constant used by the
plugin) for those addresses.
In
`@app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java`:
- Around line 249-252: The hostname validation currently discards the
validator's message and always adds the generic
DS_INVALID_ACCOUNT_NAME_ERROR_MSG; change the handling in the block that calls
JdbcHostValidator.validateHostname(datasourceConfiguration.getUrl()) so that
when the Optional is present you use the returned error string (the Optional's
value) in the invalids.add call (or append it to the formatted
SnowflakeErrorMessages.DS_INVALID_ACCOUNT_NAME_ERROR_MSG) instead of hardcoding
"invalid characters"; update the code around JdbcHostValidator.validateHostname,
datasourceConfiguration.getUrl(), invalids.add and
SnowflakeErrorMessages.DS_INVALID_ACCOUNT_NAME_ERROR_MSG to include the
validator's message.
---
Nitpick comments:
In
`@app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/JdbcHostValidatorTest.java`:
- Line 14: Add IPv6 cases to the parameterized test in JdbcHostValidatorTest by
including representative IPv6 literals (e.g., "::1", "fe80::1", and a full IPv6
like "2001:0db8:85a3::8a2e:0370:7334") in the `@ValueSource` strings so the
validator is exercised for IPv6 input; update the `@ValueSource` on the test
method in JdbcHostValidatorTest (the parameterized test annotated with
`@ValueSource`(strings = {...})) to include these entries and ensure any expected
assertions in the same test method still hold for IPv6 addresses.
In
`@app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.java`:
- Around line 184-189: Update the test
testValidateDatasource_withHashInHostname_returnsInvalid to assert the
validation message includes the exact invalid host value; after setting
dsConfig.getEndpoints().get(0).setHost("evil.com#fragment"), ensure the
assertion on pluginExecutor.validateDatasource(dsConfig) checks both the generic
message ("Host value cannot contain") and that the message echoes the host
string ("evil.com#fragment") so the error is tied to this specific input.
🪄 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: 1115dfcb-3592-4006-af59-8c58cac67cf1
📒 Files selected for processing (17)
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/exceptions/pluginExceptions/BasePluginErrorMessages.javaapp/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/JdbcHostValidator.javaapp/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/JdbcHostValidatorTest.javaapp/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.javaapp/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/exceptions/MssqlErrorMessages.javaapp/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.javaapp/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.javaapp/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySQLDatasourceValidationTest.javaapp/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.javaapp/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.javaapp/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.javaapp/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.javaapp/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.javaapp/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.javaapp/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.javaapp/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/exceptions/SnowflakeErrorMessages.javaapp/server/appsmith-plugins/snowflakePlugin/src/test/java/com/external/plugins/SnowflakePluginTest.java
app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java
Show resolved
Hide resolved
| @ParameterizedTest | ||
| @ValueSource(strings = {"evil.com#fragment", "evil.com/"}) | ||
| public void testValidateDatasource_withInvalidHostname_returnsInvalid(String hostname) { | ||
| DatasourceConfiguration dsConfig = new DatasourceConfiguration(); | ||
| Endpoint endpoint = new Endpoint(); | ||
| endpoint.setHost(hostname); | ||
| endpoint.setPort(1433L); | ||
| dsConfig.setEndpoints(List.of(endpoint)); | ||
| DBAuth auth = new DBAuth(); | ||
| auth.setUsername("test"); | ||
| auth.setPassword("test"); | ||
| dsConfig.setAuthentication(auth); | ||
| Set<String> output = mssqlPluginExecutor.validateDatasource(dsConfig); | ||
| assertTrue( | ||
| output.stream().anyMatch(msg -> msg.contains("Host value cannot contain")), | ||
| "Expected hostname validation error for '" + hostname + "', got: " + output); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidateDatasource_withEmptyHostname_returnsInvalid() { | ||
| DatasourceConfiguration dsConfig = new DatasourceConfiguration(); | ||
| Endpoint endpoint = new Endpoint(); | ||
| endpoint.setHost(""); | ||
| endpoint.setPort(1433L); | ||
| dsConfig.setEndpoints(List.of(endpoint)); | ||
| DBAuth auth = new DBAuth(); | ||
| auth.setUsername("test"); | ||
| auth.setPassword("test"); | ||
| dsConfig.setAuthentication(auth); | ||
| Set<String> output = mssqlPluginExecutor.validateDatasource(dsConfig); | ||
| assertTrue(output.contains(MssqlErrorMessages.DS_MISSING_HOSTNAME_ERROR_MSG)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidateDatasource_withValidHostname_noHostErrors() { | ||
| DatasourceConfiguration dsConfig = new DatasourceConfiguration(); | ||
| Endpoint endpoint = new Endpoint(); | ||
| endpoint.setHost("mydb.internal.com"); | ||
| endpoint.setPort(1433L); | ||
| dsConfig.setEndpoints(List.of(endpoint)); | ||
| DBAuth auth = new DBAuth(); | ||
| auth.setUsername("test"); | ||
| auth.setPassword("test"); | ||
| dsConfig.setAuthentication(auth); | ||
| Set<String> output = mssqlPluginExecutor.validateDatasource(dsConfig); | ||
| assertTrue( | ||
| output.stream().noneMatch(msg -> msg.contains("Host value") || msg.contains("hostname")), | ||
| "Expected no hostname errors for valid host, got: " + output); | ||
| } |
There was a problem hiding this comment.
Add MSSQL plugin-level SSRF regression cases.
These tests cover invalid characters, but they don’t verify blocking of metadata/loopback/link-local hosts. Please add cases like 169.254.169.254 and 127.0.0.1 and assert the “not allowed” error path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java`
around lines 780 - 828, Add SSRF regression tests in MssqlPluginTest that call
mssqlPluginExecutor.validateDatasource with endpoints using loopback and
metadata hosts (e.g., "127.0.0.1" and "169.254.169.254") and assert the
validation output contains the plugin’s "not allowed" / forbidden-host error
path; specifically, add new `@ParameterizedTest` or `@Test` methods mirroring the
existing hostname tests (reuse Endpoint/DatasourceConfiguration/DBAuth setup)
and assert the returned Set<String> from validateDatasource(...) contains the
expected "not allowed" message (or the canonical error constant used by the
plugin) for those addresses.
...ver/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java
Outdated
Show resolved
Hide resolved
Failed server tests
|
…ource Address CodeRabbit review: add checkSsrfProtection calls to all 5 endpoint-based plugins (MSSQL, Postgres, Oracle, Redshift, MySQL), add SSRF regression tests, and simplify Snowflake error message handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failed server tests
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.java (1)
192-196: Tighten the metadata-IP assertion to reduce false positives.At Line 196, matching only
"not allowed"could pass on an unrelated validation message. Include the blocked IP in the predicate.Proposed assertion tweak
- assertTrue(output.stream().anyMatch(msg -> msg.contains("not allowed"))); + assertTrue(output.stream().anyMatch( + msg -> msg.contains("not allowed") && msg.contains("169.254.169.254")));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.java` around lines 192 - 196, Tighten the assertion in testValidateDatasource_withMetadataIp_returnsInvalid: when calling pluginExecutor.validateDatasource(dsConfig) (where dsConfig host is set to "169.254.169.254"), change the predicate so it asserts that the returned message both mentions the validation failure and includes the blocked IP string "169.254.169.254" (e.g., anyMatch(msg -> msg.contains("not allowed") && msg.contains("169.254.169.254"))), ensuring the test only passes for messages specifically about that IP.
🤖 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-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.java`:
- Around line 192-196: Tighten the assertion in
testValidateDatasource_withMetadataIp_returnsInvalid: when calling
pluginExecutor.validateDatasource(dsConfig) (where dsConfig host is set to
"169.254.169.254"), change the predicate so it asserts that the returned message
both mentions the validation failure and includes the blocked IP string
"169.254.169.254" (e.g., anyMatch(msg -> msg.contains("not allowed") &&
msg.contains("169.254.169.254"))), ensuring the test only passes for messages
specifically about that IP.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 66bc0065-81a0-4575-a8c1-3853d8a0757f
📒 Files selected for processing (11)
app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.javaapp/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.javaapp/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/utils/MySqlDatasourceUtils.javaapp/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySQLDatasourceValidationTest.javaapp/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.javaapp/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.javaapp/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresDatasourceValidationTest.javaapp/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.javaapp/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.javaapp/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.javaapp/server/appsmith-plugins/snowflakePlugin/src/test/java/com/external/plugins/SnowflakePluginTest.java
✅ Files skipped from review due to trivial changes (3)
- app/server/appsmith-plugins/snowflakePlugin/src/main/java/com/external/plugins/SnowflakePlugin.java
- app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java
- app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java
🚧 Files skipped from review as they are similar to previous changes (6)
- app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java
- app/server/appsmith-plugins/snowflakePlugin/src/test/java/com/external/plugins/SnowflakePluginTest.java
- app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySQLDatasourceValidationTest.java
- app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java
- app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java
- app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java
The integration test uses localhost (from Testcontainers) which is correctly blocked by SSRF protection. Change assertEquals to assertTrue so the test validates the password error without being sensitive to the additional SSRF error on loopback addresses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failed server tests
|
The integration tests use localhost (from Testcontainers) which is correctly blocked by SSRF protection. Change assertTrue(output.isEmpty()) to assertFalse(output.contains(...)) so the tests validate password handling without being sensitive to the SSRF error on loopback addresses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failed server tests
|
Summary
jdbc:snowflake://URLs without validation. Injecting#in the account name caused a URI fragment that redirected the JDBC connection to an arbitrary attacker-controlled hostFixes https://linear.app/appsmith/issue/APP-15077/security-high-ssrf-and-credential-theft-via-unsanitized-hostname-in
Changes
JdbcHostValidatorinappsmith-interfaces— validates hostnames against URI-special characters (/,:,#,@,?,\) that can alter JDBC URL semanticsvalidateDatasource()— blocks the reported#fragment injection attackcontains("/") || contains(":")checks with the centralizedJdbcHostValidatorutility, expanding the blocked character set to include#,@,?,\BasePluginErrorMessagesAutomation
/ok-to-test tags="@tag.All"
Test plan
JdbcHostValidatorTest— 18 tests: validates allowed hostnames, blocks null/empty, blocks all disallowed characters, SSRF protection for metadata IPs, allows RFC 1918 private IPsSnowflakePluginTest— new parameterized tests for invalid account names (evil.com#,evil.com:443,evil.com/path) and valid account names (myaccount,myaccount.us-east-1.aws)MssqlPluginTest— new tests for empty hostname, invalid characters, and valid hostnamePostgresDatasourceValidationTest— new test for#in hostnameRedshiftPluginTest— new test for#in hostnameMySQLDatasourceValidationTest— new test for#in hostname🤖 Generated with Claude Code
Tip
🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/24123640460
Commit: 4bad559
Cypress dashboard.
Tags:
@tag.AllSpec:
Wed, 08 Apr 2026 08:50:19 UTC
Summary by CodeRabbit
New Features
Bug Fixes
Tests