Skip to content

fix: harden the Ask AI CE port after CodeRabbit review - #42092

Merged
subrata71 merged 10 commits into
releasefrom
fix/ask-ai-coderabbit-findings
Aug 6, 2026
Merged

fix: harden the Ask AI CE port after CodeRabbit review#42092
subrata71 merged 10 commits into
releasefrom
fix/ask-ai-coderabbit-findings

Conversation

@subrata71

@subrata71 subrata71 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Description

CodeRabbit reviewed appsmith-ee#9417 roughly 30 seconds after its dependency merged, so it saw the stale 39-file diff that still contained the whole Ask AI CE sync from #42065. All 15 findings landed on CE-owned client and server code that is already on release — none touch the EE shim cleanup that PR actually makes. They are fixed here, in the repo that owns the code. appsmith-ee#9417 is unaffected and stays a 10-file EE-only cleanup.

The EE repo's own pre-commit hook (check-ee-only-files.sh) independently confirms the split: it refuses any client change outside src/ee/.

Critical — a saved credential could be sent to a caller-chosen host

AIConfigServiceCEImpl.testApiKeyInternal decrypted the stored provider key when the request omitted one, then sent it to the baseUrl / endpoint supplied in that same request. Since the UI masks these keys as ••••••••, an organization admin could read one back by pointing the test at a collector they control. No transport requirement existed either, so the key could leave over plain HTTP.

The endpoint is gated on MANAGE_ORGANIZATION, so this is admin-only rather than unauthenticated — but it still recovers a secret the product deliberately hides.

The saved configuration now decides the destination whenever the key comes from storage. Only fields that cannot change the host — deployment name, API version, model — remain overridable, so an administrator can still test a different deployment against their own resource. A stored credential is no longer sent anywhere that would put it on the wire in cleartext; loopback stays exempt, and typing a key into the form still tests any endpoint explicitly.

Major

  • Encrypted credentials now carry a marker. AIConfigSecretsCE inferred "is this encrypted?" by attempting a decrypt, which answers the wrong question in both directions. After the instance encryption password changes, real ciphertext stops decrypting — it was reported as cleartext, handed to the provider as the API key, and re-encrypted by the next write, at which point the original was unrecoverable. Conversely, cleartext that happened to decrypt was treated as encrypted and never protected. A marked value that will not decrypt now raises instead of yielding its own ciphertext.
  • Migration076 normalises pre-marker values. Cleartext is encrypted; ciphertext written by a build that encrypted without marking keeps its bytes and simply gains the marker, so it is never encrypted twice. The change unit is edited in place rather than stacked because it is in no release tag and has therefore never run.
  • Ask AI failures report a real HTTP status. Everything was rewritten to INVALID_PARAMETER, so a provider outage, an invalid key and a throttled caller all returned 400 — outages stayed out of server-error metrics, and clients could not tell "back off" from "bad request". Errors keep their own class now, unexpected ones are chained as the cause, and throttling (ours and the provider's) returns 429.
  • Submissions are serialised. The editor panel dispatched independently from the send button and each quick-action chip, with the chips live during a request, so two clicks meant duplicate chat entries and a second billable provider call.

Minor

Resize listeners no longer survive an unmount mid-drag in either panel; Escape closes the global panel wherever focus sits, matching its own "Close (Esc)" tooltip; Clear Chat is disabled during a request; the context label follows the caret instead of showing the line the panel opened on; graphql-js is recognised by isAISupportedMode, so Ask AI works in GraphQL editors; the config update path maps NO_RESOURCE_FOUND to an authorization refusal rather than a 404; and the Azure error body is no longer logged a second time uncapped.

Verification

  • 21 server unit tests pass, 12 of them new: AIConfigSecretsCETest (9) pins the marker, the round trip, migration normalisation, and the loud failure on a changed encryption password; AIConfigServiceCEImplTest (3 new) pins destination selection by giving the request an HTTPS host and the saved config an HTTP one, so the refusal proves the saved value won.
  • tsc --noEmit reports no errors in any changed file (only the pre-existing packages/ast and packages/design-system failures that are on release too).
  • ESLint: 0 errors on all four changed client files. Prettier and Spotless clean.

Linear: https://linear.app/appsmith/issue/APP-15756

Slack thread: https://theappsmith.slack.com/archives/C09NG5BJ18S/p1785930741904329

Automation

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

Communication

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

  • Yes
  • No

Tip

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


Wed, 05 Aug 2026 22:13:45 UTC

Summary by CodeRabbit

  • New Features

    • AI context now shows the current editor line.
    • Stored AI provider keys can be tested without re-entering them.
    • GraphQL editor support is more consistent.
    • Destination changes warn when stored credentials will be removed.
  • Bug Fixes

    • AI submissions and chat clearing are disabled while requests process.
    • Improved AI error messages and rate-limit handling.
    • Prevented credentials from being sent to unsafe destinations.
    • Improved panel keyboard shortcuts and drag behavior.
    • Strengthened encryption handling for stored AI credentials.

When the API key test request omitted a key, the server decrypted the saved
credential and then sent it to the baseUrl or endpoint supplied in that same
request. The UI masks these keys, so anyone who could reach the endpoint could
read one back by pointing the test at a collector they control.

The saved configuration now decides the destination whenever the key comes from
storage. Only fields that cannot change the host — deployment name, API version,
model — stay overridable, so an administrator can still try a different
deployment against their own resource.

A stored credential is also no longer sent anywhere that would put it on the
wire in cleartext. Loopback stays exempt so a proxy on the Appsmith host keeps
working, and typing a key into the form still tests any endpoint explicitly.
Ciphertext and cleartext are both just strings, and "does it decrypt?" answered
the wrong question in both directions. Once the instance encryption password
changed, real ciphertext stopped decrypting: it was reported as cleartext, sent
to the provider as the API key, and re-encrypted by the next write — after
which the original credential was unrecoverable. The administrator saw only
"invalid API key". In the other direction, cleartext that happened to decrypt
without throwing was treated as already encrypted and stayed in cleartext.

Encrypted values now carry an explicit marker. A marked value that will not
decrypt raises instead of yielding its own ciphertext, so a changed encryption
password surfaces as itself. Migration076 normalises everything written before
the marker existed: cleartext is encrypted, and ciphertext from a build that
encrypted without marking keeps its bytes and simply gains the marker, so it is
never encrypted a second time.
Every AI failure was rewritten to INVALID_PARAMETER, so a provider outage, an
invalid API key and a throttled caller all came back as 400. Provider outages
stayed out of server-error metrics — the very problem the surrounding comment
set out to fix — and a client could not tell "back off and retry" from "you
sent something invalid". onErrorMap also dropped the original throwable, losing
the stack trace for anything the call sites had not already logged.

Failures now keep their own error class, and unexpected ones are chained as the
cause rather than discarded. Throttling — both our own limiter and a provider
429 — reports 429 instead of masquerading as a bad request or a server fault.
The same treatment applies to updateAIConfig, where a database, serialization
or encryption failure had been reported to the caller as invalid input.

Authorization denial there was also only half-handled: the permission-aware
lookup ends in NO_RESOURCE_FOUND, and only ACL_NO_RESOURCE_FOUND was matched,
so a caller without MANAGE_ORGANIZATION got a 404 rather than the intended
refusal. Both codes are now recognised.

Also drops a duplicate log of the Azure error body. It was already logged one
line above, capped at 1000 characters; the second copy was uncapped, so a large
provider error body landed in the log in full.
The editor panel let a second request start while the first was still running:
the send button and every quick-action chip dispatched independently, and the
chips stayed active during a request. Two clicks queued two prompts against a
single loading flag — duplicate chat entries and a second billable provider
call. All submissions now funnel through one guarded helper, and the controls
are disabled while a request is in flight. The quick-action path also loses a
100ms setTimeout it never needed.

Alongside that:

- Both panels attached mousemove and mouseup to the document when a resize
  began and only detached them on mouseup. A panel that unmounted mid-drag —
  which the route-change effect causes directly — left both listeners attached
  for the rest of the session, calling setPanelWidth on a dead component.
- The global panel advertises "Close (Esc)" but only listened on the prompt
  textarea, so the shortcut did nothing once focus moved to a chip or the
  response area.
- Its Clear Chat chip was the one control not disabled during a request, so a
  conversation could be cleared while a response was in flight and the saga
  would append that response to an emptied list.
- The editor panel's context label recomputed only when the editor or mode
  changed, so it kept showing the line the panel opened on while submission
  sent the real caret position. It now follows the caret.
- GraphQL editors run in mode "graphql-js", which isAISupportedMode did not
  recognise, so Ask AI was disabled there even though getAIContext supported
  it. Both now share one predicate.
- The AI settings test buttons required a newly typed key, leaving an
  administrator unable to test the key already stored.
@coderabbitai

coderabbitai Bot commented Aug 5, 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

This PR updates AI editor panels, provider key testing, AI secret normalization, stored-key destination validation, credential binding, and server-side AI error mapping. It adds tests for secret handling and stored-destination API-key checks.

Changes

AI assistant updates

Layer / File(s) Summary
Editor prompt flow and context
app/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx, app/client/src/ce/components/editorComponents/GPT/trigger.tsx, app/client/src/ce/components/editorComponents/GlobalAISidePanel/index.tsx
The panels track caret context, centralize prompt submission, prevent concurrent requests, clean up drag listeners, handle Escape at document level, and disable chat actions while loading. GraphQL mode detection is shared.
Secret marker handling and migration
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/AIConfigSecretsCE.java, app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration076EncryptAIAssistantApiKeys.java, app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/AIConfigSecretsCETest.java
Encrypted values use the enc:v1: marker. Storage normalization handles cleartext, legacy ciphertext, and unreadable ciphertext. Migration and credential writes use normalization. Tests cover these cases and destination policy.
Stored-key testing and provider validation
app/client/src/pages/AdminSettings/AI/index.tsx, app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java, app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIAssistantServiceCEImpl.java, app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/OrganizationServiceCEImpl.java, app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AIConfigServiceCEImplTest.java
Provider tests use saved destinations and stored keys. Destination changes clear stored keys without replacements. Stored destinations undergo HTTPS validation. Provider throttling and HTTP 429 responses return TOO_MANY_REQUESTS.
Controller error mapping
app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/AIConfigControllerCE.java, app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/UserControllerCE.java
Controllers preserve application statuses and map unexpected or invalid-credential errors according to type.

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

Sequence Diagram(s)

sequenceDiagram
  participant AdminSettingsAI
  participant AIConfigServiceCEImpl
  participant AIConfigSecretsCE
  participant ProviderEndpoint
  AdminSettingsAI->>AIConfigServiceCEImpl: test stored API key
  AIConfigServiceCEImpl->>AIConfigSecretsCE: validate saved destination
  AIConfigSecretsCE-->>AIConfigServiceCEImpl: allow or reject endpoint
  AIConfigServiceCEImpl->>ProviderEndpoint: send request to saved endpoint
Loading

Possibly related issues

  • appsmithorg/appsmith-ee#9431 — Covers the credential-handling, error-status, Ask AI panel, GraphQL-mode, and listener changes in this PR.
  • appsmithorg/appsmith-ee#9395 — Covers the AI security, encryption, SSRF protection, authorization, and API-key lifecycle changes.

Possibly related PRs

Suggested labels: Security

Suggested reviewers: sondermanish

Poem

The caret tracks its line,
Stored keys bind by design.
Secrets wear enc:v1: bright,
Errors map to statuses right.
Drag listeners leave cleanly.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: hardening the Ask AI CE implementation after review.
Description check ✅ Passed The description covers motivation, scope, verification, automation, communication, and relevant links with sufficient implementation detail.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ask-ai-coderabbit-findings

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

@github-actions github-actions Bot added the Bug Something isn't working label Aug 5, 2026
@subrata71 subrata71 self-assigned this Aug 5, 2026
@subrata71 subrata71 added the ok-to-test Required label for CI label Aug 5, 2026
A marked value that will not decrypt means the instance encryption password
changed. That is a server-side condition, so INVALID_PARAMETER was the wrong
class for it — the caller got a non-retryable 4xx and the failure stayed out of
server-error metrics, which is the same objection this branch already raised
against the AIConfigControllerCE mapping.

No message is attached, for two reasons. INTERNAL_SERVER_ERROR has no
placeholder, so one would be silently dropped. And "re-enter the API key" would
point at a fix that resolves the symptom for one credential while leaving every
other secret encrypted with the previous password unreadable. The log line
carries the real cause instead.

Reported by CodeRabbit on appsmith-ee#9417.
@subrata71
subrata71 marked this pull request as ready for review August 5, 2026 16:11
@subrata71
subrata71 requested a review from a team as a code owner August 5, 2026 16:11

@hacktron-app hacktron-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 1 file

Severity Count
HIGH 1

View full scan results

Comment thread app/client/src/pages/AdminSettings/AI/index.tsx

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

🤖 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/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx`:
- Line 415: Apply the loading guard to every Clear Chat entry point by updating
the AISidePanel and GlobalAISidePanel clear-chat flows: disable the header
ClearChatButton/ClearButton when isLoading is true, and add an early return in
each handleClearChat handler so clear actions are rejected while loading. In
app/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx#L415-L415 and
app/client/src/ce/components/editorComponents/GlobalAISidePanel/index.tsx#L426-L426,
keep the existing quick-action chip behavior consistent with the header
controls.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/AIConfigSecretsCE.java`:
- Around line 90-96: Update the ciphertext migration logic around
EncryptionHelper.decrypt and encrypt so a decryption failure preserves the
original stored value instead of re-encrypting it. Keep the successful decrypt
path adding ENCRYPTED_PREFIX, and ensure Migration076EncryptAIAssistantApiKeys
receives unreadable values unchanged for explicit recovery.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java`:
- Around line 850-887: Enforce the stored-credential HTTPS policy across the
request paths in AIConfigServiceCEImpl, including callClaudeAPI, callOpenAIAPI,
and callAzureOpenAIAPI, before any stored key is attached. Update
allowsStoredCredential to reject all HTTP destinations, including loopback
addresses, while preserving local HTTP testing only when the key was explicitly
entered in the request rather than loaded from saved configuration.
🪄 Autofix

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: a8079a39-f332-4960-9948-b7d3b6ae7c0c

📥 Commits

Reviewing files that changed from the base of the PR and between bfc8c82 and b78c835.

📒 Files selected for processing (12)
  • app/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx
  • app/client/src/ce/components/editorComponents/GPT/trigger.tsx
  • app/client/src/ce/components/editorComponents/GlobalAISidePanel/index.tsx
  • app/client/src/pages/AdminSettings/AI/index.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/AIConfigControllerCE.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/UserControllerCE.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/AIConfigSecretsCE.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration076EncryptAIAssistantApiKeys.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIAssistantServiceCEImpl.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/AIConfigSecretsCETest.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AIConfigServiceCEImplTest.java

Comment thread app/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx
…-key request

Three review findings, one theme: a stored provider credential must not be
destroyed, and must not leave the instance in cleartext.

normalizeForStorage encrypted anything that failed to decrypt, treating it as
cleartext. If the instance encryption password had changed before the migration
ran, valid pre-marker ciphertext failed that decrypt and was encrypted a second
time — after which neither the old password nor the new one recovers the key.
An unreadable value that still has the shape of our ciphertext (hex-encoded
AES-GCM, so 64 hex characters at minimum) is now left untouched and logged for
an operator. A cleartext key can only be misread as ciphertext if it is both
long enough and hex enough, and the consequence there is that it stays
unencrypted rather than being destroyed — the direction this has to fail in.
encryptCredentialsInPlace routes through the same decision, so the settings
write path cannot nest ciphertext either.

The HTTPS destination check only guarded the settings "test key" path, while
the Ask AI request path sent stored keys to callClaudeAPI, callOpenAIAPI and
callAzureOpenAIAPI with no check at all — a configured http:// provider URL put
the credential on the wire in cleartext on every request. The policy now lives
in AIConfigSecretsCE.allowsStoredCredential as the single definition, and both
paths call it. Loopback stays permitted, since traffic that never reaches a
network cannot expose the key, and without it a local LLM proxy is unusable.

Clear Chat was disabled as a quick-action chip but still live as a header
button in both panels, and neither handler checked. Clearing mid-request empties
the list the saga is about to append the in-flight response to, leaving an
answer with no question above it. Both controls are disabled and both handlers
guarded.

Reported by CodeRabbit and Hacktron on appsmith-ee#9417 and #42092.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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-server/src/main/java/com/appsmith/server/helpers/ce/AIConfigSecretsCE.java`:
- Around line 95-107: Update allowsStoredCredential and the Azure, Claude, and
OpenAI WebClientUtils call paths so the permitted loopback exception is
reachable only through a client path that validates the hostname before
connection and the resolved remote IP after connection; otherwise remove the
loopback allowance from allowsStoredCredential and update its documentation and
tests accordingly.
- Around line 129-138: Update normalizeForStorage and encryptIfNeeded so a valid
64-character hexadecimal credential is treated as cleartext and encrypted,
rather than classified as unreadable ciphertext and retained unchanged. Preserve
the explicit recovery state for genuinely ambiguous legacy ciphertext. Add
coverage verifying that a 64-character hexadecimal cleartext key is encrypted
and persists correctly.
🪄 Autofix

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: 29b64d02-bae5-42c7-8c17-fa159ad1f520

📥 Commits

Reviewing files that changed from the base of the PR and between b78c835 and e66738b.

📒 Files selected for processing (6)
  • app/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx
  • app/client/src/ce/components/editorComponents/GlobalAISidePanel/index.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/AIConfigSecretsCE.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIAssistantServiceCEImpl.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/AIConfigSecretsCETest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/client/src/ce/components/editorComponents/GlobalAISidePanel/index.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java
  • app/client/src/ce/components/editorComponents/GPT/AISidePanel.tsx

Routing encryptIfNeeded through normalizeForStorage borrowed the migration's
caution about unreadable ciphertext into a path that must not have it. The
shape check cannot tell a 64-character hexadecimal API key from ciphertext, so
such a key was left alone and persisted to Mongo in cleartext.

The two paths answer different questions. encryptCredentialsInPlace is reached
only from PUT /organizations, where the value came from the request body and an
unmarked value is therefore always a key the caller just supplied — it is
encrypted unconditionally. normalizeForStorage reads values already in storage,
where cleartext and pre-marker ciphertext are genuinely indistinguishable once
decryption fails, and that is the only place the shape check belongs.

Reported by CodeRabbit on appsmith-ee#9417.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.configurations.OpenApiDocsAuthTest#openApiDocsEndpoint_unauthenticated_returns401
  • com.appsmith.server.configurations.OpenApiDocsAuthTest#swaggerUiEndpoint_unauthenticated_returns401

Pinning the test request to the saved destination closed only half of this.
The same key could still be redirected in two steps: save a new base URL with
the key field left blank — blank means "unchanged", so the key survives — and
the stored credential now points at a host of the caller's choosing. Testing
the key, or simply using the assistant, then sends it there. The settings page
masks these keys precisely so an administrator cannot read them back.

Changing a provider's base URL or endpoint without supplying a replacement now
clears that provider's stored key, so there is nothing left to redirect. The
form warns before saving, since the key disappearing silently would be worse
than the inconvenience of re-entering it.

Also drops the loopback exception from allowsStoredCredential. It was
unreachable: these requests all go out through WebClientUtils, whose
RestrictedHostFilter denies loopback as a literal host and again after DNS
resolution. Permitting http://localhost only described a capability the HTTP
layer refuses, so the policy is now HTTPS or nothing.

Reported by Hacktron and CodeRabbit on #42092 and appsmith-ee#9417.

@hacktron-app hacktron-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 2 files

Severity Count
HIGH 2

View full scan results

<ButtonRow style={{ marginTop: "8px" }}>
<Button
isDisabled={!claudeApiKey}
isDisabled={!claudeApiKey && !hasStoredClaudeApiKey}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Stored AI Provider API Key Exfiltration via Test Connection to Arbitrary Base URL

The pull request modifies the isDisabled property of the "Test Key" buttons for AI providers (Claude, OpenAI, Azure OpenAI) to allow testing the connection when a stored key exists but no new key has been typed.

However, because the user can modify the "Base URL" or "Endpoint" field in the UI without saving, they can change the destination URL to an attacker-controlled server and click "Test Key". When clicked, the application triggers handleTestApiKey, which sends the modified URL and an undefined API key to the backend. The backend then decrypts the stored API key and sends it in an outbound request to the newly specified arbitrary URL, allowing a malicious or compromised organization administrator to exfiltrate highly sensitive third-party API keys.

While the PR introduces a warning message on save (DestinationChangeWarning), this warning is not displayed or enforced when clicking "Test Key" on unsaved changes, leaving the stored credential vulnerable to silent exfiltration.

Steps to Reproduce
  1. Navigate to the Admin Settings > AI page as an administrator.
  2. Select an AI Provider (e.g., Claude) that already has a stored API key.
  3. Modify the 'Base URL' or 'Endpoint' field to point to an external listener or destination under your control.
  4. Click the 'Test Key' button.
  5. Verify if the backend initiates an outbound request to the modified URL containing the decrypted, stored API key.
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: app/client/src/pages/AdminSettings/AI/index.tsx
Lines: 1023
Severity: high

Vulnerability: Stored AI Provider API Key Exfiltration via Test Connection to Arbitrary Base URL

Description:
The pull request modifies the `isDisabled` property of the "Test Key" buttons for AI providers (Claude, OpenAI, Azure OpenAI) to allow testing the connection when a stored key exists but no new key has been typed. 

However, because the user can modify the "Base URL" or "Endpoint" field in the UI without saving, they can change the destination URL to an attacker-controlled server and click "Test Key". When clicked, the application triggers `handleTestApiKey`, which sends the modified URL and an `undefined` API key to the backend. The backend then decrypts the stored API key and sends it in an outbound request to the newly specified arbitrary URL, allowing a malicious or compromised organization administrator to exfiltrate highly sensitive third-party API keys.

While the PR introduces a warning message on save (`DestinationChangeWarning`), this warning is not displayed or enforced when clicking "Test Key" on unsaved changes, leaving the stored credential vulnerable to silent exfiltration.

Proof of Concept:
**Steps to Reproduce**

1. Navigate to the Admin Settings > AI page as an administrator.
2. Select an AI Provider (e.g., Claude) that already has a stored API key.
3. Modify the 'Base URL' or 'Endpoint' field to point to an external listener or destination under your control.
4. Click the 'Test Key' button.
5. Verify if the backend initiates an outbound request to the modified URL containing the decrypted, stored API key.

Affected Code:
                  isDisabled={!claudeApiKey && !hasStoredClaudeApiKey}

Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.

Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), !accepted_risk <reason>, or !fixed (resolved). Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.

View finding in Hacktron

Comment on lines +143 to +162
clearKeyIfDestinationChanged(
previousClaudeBaseUrl,
assistantConfig.getClaudeBaseUrl(),
aiConfig.getClaudeApiKey(),
assistantConfig::setClaudeApiKey);
clearKeyIfDestinationChanged(
previousOpenaiBaseUrl,
assistantConfig.getOpenaiBaseUrl(),
aiConfig.getOpenaiApiKey(),
assistantConfig::setOpenaiApiKey);
clearKeyIfDestinationChanged(
previousAzureOpenaiEndpoint,
assistantConfig.getAzureOpenaiEndpoint(),
aiConfig.getAzureOpenaiApiKey(),
assistantConfig::setAzureOpenaiApiKey);
clearKeyIfDestinationChanged(
previousCopilotEndpoint,
assistantConfig.getCopilotEndpoint(),
aiConfig.getCopilotApiKey(),
assistantConfig::setCopilotApiKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH Logical Bypass in AI Credential Destination Binding Allows Leakage of Stored API Keys

A logical bypass exists in the AI credential destination binding mechanism (clearKeyIfDestinationChanged) due to unhandled cross-field fallback relationships between Azure OpenAI and Copilot configurations. Specifically, azureOpenaiEndpoint falls back to copilotEndpoint if empty, and azureOpenaiApiKey falls back to copilotApiKey if empty. An attacker with organization management permissions can exploit this fallback logic to change one of the endpoints to an external destination without triggering the credential-clearing logic for the fallback key. When the API key is subsequently tested or used, the decrypted stored credential is sent to the attacker-controlled destination, leading to credential exfiltration.

Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java
Lines: 143-162
Severity: high

Vulnerability: Logical Bypass in AI Credential Destination Binding Allows Leakage of Stored API Keys

Description:
A logical bypass exists in the AI credential destination binding mechanism (clearKeyIfDestinationChanged) due to unhandled cross-field fallback relationships between Azure OpenAI and Copilot configurations. Specifically, azureOpenaiEndpoint falls back to copilotEndpoint if empty, and azureOpenaiApiKey falls back to copilotApiKey if empty. An attacker with organization management permissions can exploit this fallback logic to change one of the endpoints to an external destination without triggering the credential-clearing logic for the fallback key. When the API key is subsequently tested or used, the decrypted stored credential is sent to the attacker-controlled destination, leading to credential exfiltration.

Affected Code:
                    clearKeyIfDestinationChanged(
                            previousClaudeBaseUrl,
                            assistantConfig.getClaudeBaseUrl(),
                            aiConfig.getClaudeApiKey(),
                            assistantConfig::setClaudeApiKey);
                    clearKeyIfDestinationChanged(
                            previousOpenaiBaseUrl,
                            assistantConfig.getOpenaiBaseUrl(),
                            aiConfig.getOpenaiApiKey(),
                            assistantConfig::setOpenaiApiKey);
                    clearKeyIfDestinationChanged(
                            previousAzureOpenaiEndpoint,
                            assistantConfig.getAzureOpenaiEndpoint(),
                            aiConfig.getAzureOpenaiApiKey(),
                            assistantConfig::setAzureOpenaiApiKey);
                    clearKeyIfDestinationChanged(
                            previousCopilotEndpoint,
                            assistantConfig.getCopilotEndpoint(),
                            aiConfig.getCopilotApiKey(),
                            assistantConfig::setCopilotApiKey);

Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.

Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), !accepted_risk <reason>, or !fixed (resolved). Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.

View finding in Hacktron

@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

🤖 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-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java`:
- Around line 83-89: Update AIConfigServiceCEImpl’s credential-destination
handling around the previous endpoint captures and clearKeyIfDestinationChanged:
resolve effective endpoint/key-source pairs before and after the update,
including Azure/Copilot fallback behavior and provider defaults for null
Claude/OpenAI destinations. Clear every stored key whose effective destination
changed unless the request supplies its replacement, covering both Azure/Copilot
fallback directions. Add regression tests for those cases and null-to-default
Claude/OpenAI saves.
🪄 Autofix

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: f790f7fb-cf85-46c2-bc00-d138cba09e8e

📥 Commits

Reviewing files that changed from the base of the PR and between 83cb2ce and 0942c37.

📒 Files selected for processing (5)
  • app/client/src/pages/AdminSettings/AI/index.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/AIConfigSecretsCE.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AIConfigServiceCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/AIConfigSecretsCETest.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AIConfigServiceCEImplTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/AIConfigSecretsCETest.java

The previous attempt compared four independent raw field pairs, which got the
rule wrong in both directions.

It under-enforced. Azure resolves its key as azureOpenaiApiKey then
copilotApiKey, and its destination as azureOpenaiEndpoint then copilotEndpoint,
and the COPILOT provider reaches the same code. Treating those four fields as
two unrelated pairs meant a new Azure endpoint left a stored Copilot key in
place, and the read paths then put the two together.

It also over-enforced. Stored claudeBaseUrl may be null while the client always
submits the provider default, so a save that changed only a model name looked
like a destination change and deleted a working key, with no warning shown.

Both now resolve the effective destination — the fallback chain the read paths
walk, falling back to the provider default — and compare that. When it moves,
every key in the group is invalidated unless the same write supplied a
replacement for it.

The rule also runs on the generic PUT /organizations path. That endpoint merges
sparse request fields into the saved document, so a provider URL arriving there
would move underneath a key that stays put — the same redirection /ai-config
already refuses. Taking whole before/after configurations rather than request
fields lets one definition cover both writers.

Two smaller defects alongside: validateApiKey now ignores the masked
placeholder, which the settings page can echo back and which would otherwise
overwrite a working key with bullets; and allowsStoredCredential now requires a
host, so "https:///path" no longer passes as a destination.

Reviewed against the CWE Top 25 with a second model. Deferred and tracked:
persisting each key's bound destination so it survives a change to the default
URL constants, and optimistic concurrency for two admins saving at once.

Reported by Hacktron and CodeRabbit on #42092 and appsmith-ee#9417.
@subrata71
subrata71 merged commit adc82dd into release Aug 6, 2026
84 checks passed
@subrata71
subrata71 deleted the fix/ask-ai-coderabbit-findings branch August 6, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working ok-to-test Required label for CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants