Skip to content

Conversation

@Logrui
Copy link

@Logrui Logrui commented Dec 21, 2025

Update decryption logic to only apply to CREDENTIAL_TYPE variables, ensuring other types are treated as plaintext.

Bug Fix: Langflow Variable Decryption Mismatch (InvalidToken)

Date: 2025-12-21
Status: Resolved

1. Issue Description

  • Symptoms:
    • Backend logs showing InvalidToken errors: Decryption using UTF-8 encoded API key failed. Error: . Retrying decryption using the raw string input.
    • Decryption of Generic failed for variable ... Assuming plaintext.
    • POST /api/v1/custom_component/update HTTP/1.1" 400 errors during component updates.
    • Error: Invalid base64-encoded string: number of data characters (21) cannot be 1 more than a multiple of 4.
  • Context: Occurred when update_params_with_load_from_db_fields was called, triggering get_variable or get_all in DatabaseVariableService. This happens frequently when the UI tries to load variable values for components.

2. Root Cause Analysis

  • Investigation:
    • Analyzed the DatabaseVariableService.get_variable and get_all methods in service.py.
    • Observed that get_variable was unconditionally calling auth_utils.decrypt_api_key(variable.value) regardless of the variable type.
    • Observed that get_all was attempting to decrypt GENERIC_TYPE variables (labeled as "attempt to decrypt") but failing because they were plaintext.
  • The Cause:
    • Symmetry violation: create_variable and update_variable only encrypt variables if type == CREDENTIAL_TYPE.
    • However, get_variable and get_all tried to decrypt everything.
    • When a GENERIC_TYPE variable (stored as plaintext) was passed to the Fernet decryptor, it raised InvalidToken because the plaintext string was not a valid Fernet token (base64 encoded).

3. The Fix

  • Changes Made:
    • Modified src/backend/base/langflow/services/variable/service.py.
    • Updated get_variable to check if variable.type == CREDENTIAL_TYPE before decrypting.
    • Updated get_all to check if variable.type == CREDENTIAL_TYPE before decrypting, and treat others as plaintext.
  • Code Snippet:
    # After change in get_variable
    # Only decrypt CREDENTIAL_TYPE variables - others are stored as plaintext
    if variable.type == CREDENTIAL_TYPE:
        return auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
    return variable.value

4. Verification

  • Test Case:
    1. Rebuilt the container with the code fix.
    2. Monitored logs during startup and API interactions.
  • Outcome:
    • The InvalidToken and Decryption failed error logs disappeared.
    • The 400 errors on custom_component/update should now be resolved as the variables are correctly returned as plaintext without throwing exceptions.

Summary by CodeRabbit

  • Bug Fixes
    • Improved variable handling to properly distinguish between credential and non-credential types during decryption.
    • Enhanced security by preventing exposure of corrupted data when decryption fails; affected values are now safely treated as inaccessible.

✏️ Tip: You can customize this high-level summary in your review settings.

Update decryption logic to only apply to CREDENTIAL_TYPE variables, ensuring other types are treated as plaintext.
Copilot AI review requested due to automatic review settings December 21, 2025 20:48
@github-actions github-actions bot added the community Pull Request from an external contributor label Dec 21, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 21, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Modified variable service decryption logic to apply decryption selectively. get_variable and get_all methods now decrypt only CREDENTIAL_TYPE variables, returning other types as plaintext. On decryption failure, values are set to None instead of falling back to plaintext exposure.

Changes

Cohort / File(s) Summary
Variable Service Decryption Logic
src/backend/base/langflow/services/variable/service.py
Restricted decryption to CREDENTIAL_TYPE variables in get_variable and get_all methods. Non-credential types now returned as plaintext without decryption attempts. Changed error handling to return None on decryption failure instead of plaintext fallback. Updated error messaging to reflect None assignment on failure.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review decryption logic changes to ensure CREDENTIAL_TYPE filtering is applied correctly in both methods
  • Verify error handling change (None vs. plaintext fallback) doesn't introduce unexpected behavior or break dependent code
  • Confirm plaintext exposure risks are mitigated by new None assignment on decryption failures
  • Check that existing tests cover the modified branches and new conditional paths

Pre-merge checks and finishing touches

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 3 warnings)
Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error PR lacks test coverage for error handling in get_all() decryption failures and regression tests for the InvalidToken bug fix. Add tests for decryption failure handling in get_all(), regression tests for plaintext GENERIC_TYPE variables, and error handling tests for get_variable().
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Test Quality And Coverage ⚠️ Warning Pull request introduces new conditional decryption branches in get_variable and get_all without corresponding unit tests. Add pytest tests using pytest-asyncio for get_variable and get_all that mock decrypt_api_key and cover success, failure, and plaintext scenarios.
Test File Naming And Structure ⚠️ Warning Test file lacks critical coverage for substantially modified get_all() method and new GENERIC_TYPE plaintext handling scenarios. Add tests for: (1) get_all() with CREDENTIAL_TYPE and GENERIC_TYPE variables, (2) get_variable() with GENERIC_TYPE, (3) decryption failures in get_all(), (4) mixed variable types.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: restricting decryption logic to CREDENTIAL_TYPE variables only, which is the core bug fix in the changeset.
Excessive Mock Usage Warning ✅ Passed Test files demonstrate appropriate use of mocking with real object instantiation for core business logic and mocks only for external dependencies.

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 github-actions bot added the bug Something isn't working label Dec 21, 2025
@github-actions github-actions bot added bug Something isn't working and removed bug Something isn't working labels Dec 21, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical bug where the variable decryption logic was incorrectly attempting to decrypt all variable types, causing InvalidToken errors for non-credential variables that are stored as plaintext. The fix ensures that only CREDENTIAL_TYPE variables undergo decryption, while other types (like GENERIC_TYPE) are returned as-is.

Key Changes:

  • Modified get_variable() to conditionally decrypt only CREDENTIAL_TYPE variables
  • Updated get_all() to reverse the type check logic, decrypting credentials and treating other types as plaintext
  • Removed unused GENERIC_TYPE import

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

)
value = variable.value
await logger.adebug(f"Decryption of {variable.type} failed for variable '{variable.name}': {e}.")
value = None # Don't expose potentially corrupted credential data
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

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

Setting value = None when credential decryption fails could break consumers expecting a string value. Consider raising an exception or logging the error and returning a clear error indicator instead of silently returning None, which may cause downstream null reference issues.

Suggested change
value = None # Don't expose potentially corrupted credential data
value = "<DECRYPTION_FAILED>" # Don't expose potentially corrupted credential data

Copilot uses AI. Check for mistakes.
f"Decryption of {variable.type} failed for variable '{variable.name}': {e}. Assuming plaintext."
)
value = variable.value
await logger.adebug(f"Decryption of {variable.type} failed for variable '{variable.name}': {e}.")
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

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

The error message for failed credential decryption should provide more actionable guidance. Consider including information about potential causes (corrupted data, key rotation) and next steps for the user.

Suggested change
await logger.adebug(f"Decryption of {variable.type} failed for variable '{variable.name}': {e}.")
await logger.adebug(
"Failed to decrypt credential variable '%s' of type '%s': %s. "
"This can happen if the encryption key or settings have changed, or if the stored data "
"is corrupted. Ask the user to re-enter this credential or verify the encryption/key "
"configuration before retrying.",
variable.name,
variable.type,
e,
)

Copilot uses AI. Check for mistakes.
Copy link
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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/backend/base/langflow/services/variable/service.py (1)

14-14: Remove unused import to fix pipeline failure.

The GENERIC_TYPE import is flagged as unused by the Ruff style checker, causing the pipeline to fail.

🔎 Proposed fix
-from langflow.services.variable.constants import CREDENTIAL_TYPE, GENERIC_TYPE
+from langflow.services.variable.constants import CREDENTIAL_TYPE
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1e51b and f768c20.

📒 Files selected for processing (1)
  • src/backend/base/langflow/services/variable/service.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/backend_development.mdc)

src/backend/**/*.py: Use FastAPI async patterns with await for async operations in component execution methods
Use asyncio.create_task() for background tasks and implement proper cleanup with try/except for asyncio.CancelledError
Use queue.put_nowait() for non-blocking queue operations and asyncio.wait_for() with timeouts for controlled get operations

Files:

  • src/backend/base/langflow/services/variable/service.py
🧬 Code graph analysis (1)
src/backend/base/langflow/services/variable/service.py (2)
src/backend/base/langflow/services/auth/utils.py (1)
  • decrypt_api_key (567-593)
src/backend/base/langflow/services/database/models/variable/model.py (2)
  • VariableRead (52-64)
  • Variable (25-45)
🪛 GitHub Actions: Ruff Style Check
src/backend/base/langflow/services/variable/service.py

[error] 14-14: F401 'GENERIC_TYPE' imported but unused.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 5
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 2
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 1
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 4
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 3
  • GitHub Check: Test Docker Images / Test docker images
  • GitHub Check: Lint Backend / Run Mypy (3.13)
  • GitHub Check: Lint Backend / Run Mypy (3.11)
  • GitHub Check: Run Frontend Tests / Determine Test Suites and Shard Distribution
  • GitHub Check: Lint Backend / Run Mypy (3.10)
  • GitHub Check: Run Backend Tests / Integration Tests - Python 3.10
  • GitHub Check: Lint Backend / Run Mypy (3.12)
  • GitHub Check: Run Backend Tests / LFX Tests - Python 3.10
  • GitHub Check: Test Starter Templates
  • GitHub Check: Agent
  • GitHub Check: CodeQL analysis (python)
  • GitHub Check: Update Starter Projects
  • GitHub Check: Update Component Index
  • GitHub Check: Optimize new Python code in this PR
🔇 Additional comments (1)
src/backend/base/langflow/services/variable/service.py (1)

187-202: LGTM! Secure error handling and correct type-based decryption.

The updated logic correctly:

  • Decrypts only CREDENTIAL_TYPE variables
  • Sets value to None on decryption failure to prevent exposing potentially corrupted credential data
  • Returns plaintext for non-credential types

Minor: The comment on line 187 mentions "Generic variables" specifically, but the logic handles all non-CREDENTIAL_TYPE variables as plaintext, which is the correct behavior.

Comment on lines +179 to +182
# Only decrypt CREDENTIAL_TYPE variables - others are stored as plaintext
if variable.type == CREDENTIAL_TYPE:
return auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
return variable.value
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for decryption failures to match get_all.

The get_all method (lines 193-199) includes a try-catch block to handle decryption failures and logs the error, but get_variable does not. If decrypt_api_key raises an exception (e.g., due to corrupted data or key mismatch), it will propagate unhandled to the caller, leading to inconsistent error behavior across methods.

🔎 Proposed fix to add consistent error handling
 # Only decrypt CREDENTIAL_TYPE variables - others are stored as plaintext
 if variable.type == CREDENTIAL_TYPE:
-    return auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
+    try:
+        return auth_utils.decrypt_api_key(variable.value, settings_service=self.settings_service)
+    except Exception as e:  # noqa: BLE001
+        await logger.adebug(
+            f"Decryption of {variable.type} failed for variable '{variable.name}': {e}."
+        )
+        msg = f"Failed to decrypt variable {name}"
+        raise ValueError(msg) from e
 return variable.value

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/backend/base/langflow/services/variable/service.py around lines 179 to
182, get_variable currently calls auth_utils.decrypt_api_key directly for
CREDENTIAL_TYPE variables without error handling; wrap the decrypt_api_key call
in a try/except that mirrors get_all: catch exceptions from decrypt_api_key, log
the error with the same logger and message format used in get_all (include
exception details), and return None (or a safe fallback) when decryption fails
so the method's behavior matches get_all's error-handling pattern.

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

Labels

bug Something isn't working community Pull Request from an external contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant