fix(federated): prevent masked credentials from corrupting stored secrets#9868
fix(federated): prevent masked credentials from corrupting stored secrets#9868
Conversation
…rets Frontend edit form no longer pre-fills credential fields with masked values, and backend rejects any credential containing mask characters as a safety net.
|
Preview Deployment
|
Greptile SummaryThis PR fixes a credential-corruption bug where the federated connector edit form pre-populated credential fields with masked values returned by the API ( Changes:
Two minor gaps in the backend guard are noted: the Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Admin
participant FederatedForm as FederatedConnectorForm (FE)
participant API as PUT /api/federated/{id}
participant DB as update_federated_connector (DB)
Admin->>FederatedForm: Opens edit page
Note over FederatedForm: credentials initialised to {}<br/>(NOT pre-filled with masked values)
Note over FederatedForm: credentialsModified = false
alt User does NOT touch credential fields
Admin->>FederatedForm: Saves (config-only change)
FederatedForm->>API: PUT { credentials: undefined, config: {...} }
API->>DB: update(credentials=None)
Note over DB: if credentials is not None → SKIPPED<br/>Existing secrets preserved ✅
else User types new credential values
Admin->>FederatedForm: Types into credential inputs
Note over FederatedForm: credentialsModified = true
Admin->>FederatedForm: Saves
FederatedForm->>API: PUT { credentials: {client_id, client_secret}, config: {...} }
API->>DB: update(credentials={...})
DB->>DB: _reject_masked_credentials()<br/>• Rejects "••••••••••••" (bullet char)<br/>• Rejects "xxxx...xxxx" (11-char format)
Note over DB: Credentials validated & persisted ✅
end
Prompt To Fix All With AIThis is a comment left during a code review.
Path: backend/onyx/db/federated.py
Line: 47-68
Comment:
**`"*****"` integer-mask format not covered**
`mask_credential_dict` (in `encryption.py`) renders `int` and `float` credential values as the string `"*****"` (five asterisks, no bullet chars, only 5 chars). `_reject_masked_credentials` currently only detects the bullet-char format and the `xxxx...xxxx` (11-char) format produced by `mask_string`, so a masked numeric field would silently pass through.
For the Slack OAuth use-case all credential values are strings, so this is not a current regression. But if a future federated connector schema includes a numeric field, a masked `"*****"` value would bypass the guard.
Consider extending the check:
```python
MASK_NUMERIC_PLACEHOLDER = "*****"
def _reject_masked_credentials(credentials: dict[str, Any]) -> None:
for key, val in credentials.items():
if isinstance(val, str) and (
MASK_CREDENTIAL_CHAR in val
or MASK_CREDENTIAL_LONG_RE.match(val)
or val == MASK_NUMERIC_PLACEHOLDER
):
raise ValueError(
f"Credential field '{key}' contains masked placeholder characters. Please provide the actual credential value."
)
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: backend/onyx/db/federated.py
Line: 47-68
Comment:
**`_reject_masked_credentials` is not recursive**
`mask_credential_dict` recurses into nested dicts and lists, so a masked credential payload can contain nested masked values (e.g. `{"oauth": {"client_id": "abcd...wxyz"}}`). The current `_reject_masked_credentials` only iterates the top-level keys, so nested masked values would pass unchecked.
For the current federated connector schemas (flat Slack OAuth dicts) this is not an issue, but it creates a gap for future schemas that use nested credential structures. Consider a recursive traversal (analogous to how `mask_credential_dict` recurses).
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "fix(federated): address PR review commen..." | Re-trigger Greptile |
backend/tests/unit/federated_connector/test_reject_masked_credentials.py
Outdated
Show resolved
Hide resolved
🖼️ Visual Regression Report
|
- Move MASK_CREDENTIAL_CHAR to constants.py (review comment) - Add long-string mask format detection via regex (Greptile P1) - Update tests to use real mask_string output format (Greptile P1) - Guard Validate button in edit mode when credentials unchanged (Greptile P2)
Description
When an admin edits a federated connector, the
GET /api/federated/{id}endpoint returns credentials withapply_mask=True, replacing secrets with••••••••••••(U+2022 BULLET). The edit form loaded these masked values into state. On save,PUT /api/federated/{id}stored the masked string as the real credentials — permanently corrupting bothclient_idandclient_secret. This caused Slack OAuth to fail with "Invalid client_id parameter" because the URL contained masked bullets instead of the real client ID.Frontend fix
Edit form no longer pre-fills credential fields with masked values in edit mode. A
credentialsModifiedflag tracks whether the user has actually typed into any credential field:nullis passed toupdateFederatedConnector, which serializes asundefined(omitted from the JSON body). The Pydantic model defaultscredentialstoNone. The API endpoint passesNoneto the DB function. The DB function'sif credentials is not None:guard skips the entire credential update block — existing credentials are untouched.Backend fix (defense-in-depth)
Both
create_federated_connectorandupdate_federated_connectornow reject any credential value containing the mask character (U+2022 BULLET), preventing masked placeholders from ever being persisted.Linear: CS-55
How Has This Been Tested?
Additional Options