Skip to content

fix: redact sensitive fields in logs and remove auto-logging of args/results (WAL-9455)#1687

Merged
albertoelias-crossmint merged 3 commits intowallets-v1from
devin/1773964118-wal-9455-redact-sensitive-logs
Mar 20, 2026
Merged

fix: redact sensitive fields in logs and remove auto-logging of args/results (WAL-9455)#1687
albertoelias-crossmint merged 3 commits intowallets-v1from
devin/1773964118-wal-9455-redact-sensitive-logs

Conversation

@albertoelias-crossmint
Copy link
Collaborator

@albertoelias-crossmint albertoelias-crossmint commented Mar 19, 2026

Description

Console logs in dev were exposing full JWTs and API keys via the WithLoggerContext decorator's automatic argument/result logging and through some explicit log statements that passed entire response objects. This PR addresses WAL-9455 with two layers of defense:

  1. Redaction utility (utils.ts): Adds redactSensitiveFields() which recursively strips known-sensitive keys (jwt, apiKey, authorization, password, privateKey, auth tokens, etc.) from log context objects before they reach any sink. Applied centrally in SdkLogger.log(). Handles Error instances explicitly to preserve non-enumerable name/message/stack properties. String values >8 chars are partially truncated (first/last 4 chars shown); shorter values are fully replaced with [REDACTED].

  2. Remove auto-logging from decorator (decorators.ts): The WithLoggerContext decorator no longer automatically logs { args } on entry or { result } on success. Error logging is preserved. Individual methods already have their own curated log statements with safe fields.

  3. Targeted log fixes: getWallet success log in client.ts now logs only address + locator instead of the full response. buildContext in wallet-factory.ts now passes only chain instead of the full args object (which could contain signer configs).

Things for reviewer to verify

  • The decorator no longer logs on async success (the .then() handler was removed along with the success log). Methods that lack their own success logging will now be silent on completion.
  • No unit tests were added for redactSensitiveFields. Consider whether test coverage is needed before merge (especially the Error instance handling path).
  • The generic "token" key was intentionally excluded from SENSITIVE_KEYS to avoid false-positive redaction of blockchain token identifiers (e.g. tokenLocator). Specific auth token variants (accesstoken, authtoken, bearertoken, idtoken, refreshtoken + underscore variants) are covered instead.
  • redactValue shows first/last 4 chars for strings >8 chars. For a 9-char secret this exposes 8 of 9 characters — this trade-off was explicitly approved by the reviewer for debugging value.

Updates since last revision

  • Removed generic "token" from SENSITIVE_KEYS per reviewer feedback — replaced with specific auth token keys (accesstoken, access_token, authtoken, auth_token, bearertoken, bearer_token, idtoken, id_token, refresh_token). Added client_secret and bearer.
  • Added Error instance handling in redactSensitiveFields to preserve name/message/stack (non-enumerable properties that were previously lost via Object.entries).
  • Restored partial truncation in redactValue (first/last 4 chars for strings >8 chars) per reviewer request.

Test plan

  • Existing test suites pass (vitest: 302+ tests across wallets, react-base, and verifiable-credentials packages)
  • Lint passes (biome check)
  • Local build passes (pnpm build:libs)
  • Manual review of diff to confirm no sensitive fields leak through the remaining log paths

Package updates

None — internal-only changes to the logger pipeline, no public API surface changes.

Link to Devin session: https://crossmint.devinenterprise.com/sessions/a88ab08eaccc4c1bab8d4b5718be6638
Requested by: @albertoelias-crossmint


Open with Devin

…results (WAL-9455)

- Add redactSensitiveFields() utility that recursively strips JWTs, API keys,
  tokens, passwords, and other credentials from log context objects
- Apply redaction in SdkLogger.log() before context reaches any sink
- Remove automatic args/result logging from WithLoggerContext decorator
  (methods already have their own curated log statements with safe fields)
- Fix buildContext in wallet-factory.ts to only pass chain instead of full args
- Fix getWallet success log in client.ts to log only address instead of full response

Co-Authored-By: Alberto Elias <alberto.elias@paella.dev>
@changeset-bot
Copy link

changeset-bot bot commented Mar 19, 2026

⚠️ No Changeset found

Latest commit: caa2e6e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration
Copy link
Contributor

Original prompt from Alberto Elias

'console logs in dev expose full jwt + api key in event emitter traces' (WAL-9455)

User instruction: @devin can you investigate this in the wallets-v1 branch of crossmint-sdk and create a plan to improve the logs?

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 45-63

Comment:
**Error instances silently reduced to `{}`**

`redactSensitiveFields` calls `Object.entries(obj)` on whatever value it encounters. For native `Error` instances (the most common thing thrown in JavaScript), `message`, `name`, and `stack` are **non-enumerable** properties, so `Object.entries(error)` returns `[]` and the function returns an empty `{}` object.

This matters because the decorator now calls:
```ts
options.logger.error(`${options.methodName} threw an error`, { error });
```
After `redactSensitiveFields` runs in `SdkLogger.log()`, the `error` key in the log context will be `{}` — the stack trace, message, and error name are completely gone. This is a silent regression in error observability.

A minimal fix would be to detect Error instances and handle them like `serializeLogArgs` already does:
```ts
export function redactSensitiveFields(obj: unknown, depth = 0): unknown {
    if (depth > MAX_REDACTION_DEPTH || obj == null || typeof obj !== "object") {
        return obj;
    }

    // Error instances have non-enumerable properties; preserve them explicitly
    if (obj instanceof Error) {
        return {
            name: obj.name,
            message: obj.message,
            stack: obj.stack,
            // Any enumerable own properties (e.g. custom error fields)
            ...redactSensitiveFields(Object.fromEntries(Object.entries(obj)), depth + 1) as object,
        };
    }
    // ... rest unchanged
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 29-34

Comment:
**Partial redaction leaks too much of short secrets**

For a string of length 9 (e.g. a 9-character password or short API key), `redactValue` returns `${value.slice(0, 4)}...${value.slice(-4)}`, which exposes 8 of the 9 characters. That effectively defeats redaction for anything close to the 8-character threshold.

The PR description calls this out as a known trade-off, but it's worth resolving before merge. For truly sensitive keys (JWTs, API keys, passwords, private keys), there is no safe use-case for showing partial content — showing `[REDACTED]` universally is unambiguously safer:

```suggestion
function redactValue(_value: unknown): string {
    return "[REDACTED]";
}
```

If correlation/debugging value from partial values is desired in the future, it can be added back with a more deliberate length threshold (e.g., only reveal last 4 chars for strings > 32 chars, similar to credit-card masking conventions).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 7-21

Comment:
**Common OAuth key names not covered**

`SENSITIVE_KEYS` includes `"refreshtoken"` (matching `refreshToken` after lowercasing), but is missing several common variants:

- `"access_token"` / `"accesstoken"` — standard OAuth access token field names
- `"client_secret"` — OAuth client secret (the set has `"secret"` which requires an exact match on the key `secret`, not `client_secret`)
- `"bearer"` — occasionally used as a context key for bearer tokens

```suggestion
const SENSITIVE_KEYS = new Set([
    "jwt",
    "apikey",
    "api_key",
    "authorization",
    "x-api-key",
    "token",
    "accesstoken",
    "access_token",
    "refreshtoken",
    "refresh_token",
    "secret",
    "client_secret",
    "password",
    "privatekey",
    "private_key",
    "credential",
    "authdata",
    "bearer",
]);
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: redact sensitiv..."

devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions
Copy link
Contributor

🔥 Smoke Test Results

Status: Passed

Statistics

  • Total Tests: 5
  • Passed: 5 ✅
  • Failed: 0
  • Skipped: 0
  • Duration: 3.22 min

✅ All smoke tests passed!

All critical flows are working correctly.


This is a non-blocking smoke test. Full regression tests run separately.

…handling, simplify redactValue

- Remove generic 'token' from SENSITIVE_KEYS to avoid false-positive redaction
  of blockchain token identifiers (e.g. tokenLocator)
- Replace with specific auth token keys: accesstoken, access_token, authtoken,
  auth_token, bearertoken, bearer_token, idtoken, id_token, refresh_token
- Add client_secret and bearer to SENSITIVE_KEYS
- Simplify redactValue to always return '[REDACTED]' (no partial leaking)
- Handle Error instances explicitly to preserve name/message/stack
  (non-enumerable properties were lost with Object.entries)

Co-Authored-By: Alberto Elias <alberto.elias@paella.dev>
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 7-31

Comment:
**`"token"` missing from `SENSITIVE_KEYS` despite PR description**

The PR description explicitly states: *"The `token` key in `SENSITIVE_KEYS` is intentionally broad — it will redact any field named `token` including non-secret tokens (e.g. pagination tokens). Confirm this trade-off is acceptable."*

However, `"token"` is not present in the set. Any log context field literally named `token` (a very common pattern for auth tokens, e.g. `{ token: jwt }`) will pass through unredacted. Since this was called out as a deliberate inclusion in the description, its absence appears to be an accidental omission rather than an intentional removal.

```suggestion
const SENSITIVE_KEYS = new Set([
    "jwt",
    "token",
    "apikey",
    "api_key",
    "authorization",
    "x-api-key",
    "accesstoken",
    "access_token",
    "authtoken",
    "auth_token",
    "bearertoken",
    "bearer_token",
    "idtoken",
    "id_token",
    "refreshtoken",
    "refresh_token",
    "secret",
    "client_secret",
    "password",
    "privatekey",
    "private_key",
    "credential",
    "authdata",
    "bearer",
]);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 35-37

Comment:
**Unused `_value` parameter in `redactValue`**

The `_value` parameter is prefixed with `_` to suppress the "unused variable" lint warning, but it's never read. This suggests the function was originally intended to do partial masking (as the PR description discusses) and was then simplified to always return `[REDACTED]`. The parameter can be removed entirely to make the intent clear.

```suggestion
function redactValue(): string {
    return "[REDACTED]";
}
```

The call sites would need to be updated too:
```ts
result[key] = redactValue();
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 48-77

Comment:
**No unit tests for `redactSensitiveFields`**

This function is the core of the security fix and has several non-trivial code paths (array handling, `Error` instance serialisation, recursive depth capping, case-insensitive key matching). The existing custom engineering rule requires adding unit tests when implementing new validation logic or error handling features.

Given the security-sensitive nature of this function — a regression here could silently re-expose credentials — test coverage would be particularly valuable. Key cases to cover include:
- A top-level sensitive key is redacted
- Nested objects are recursively redacted
- `Error` instances retain `name`/`message`/`stack`
- Arrays are recursively processed
- Depth capping stops at `MAX_REDACTION_DEPTH`
- Non-sensitive keys pass through unchanged

**Rule Used:** Add unit tests when implementing new validation lo... ([source](https://app.greptile.com/review/custom-context?memory=9dc35ad5-5868-49a3-bbcb-a42edc5ee697))

**Learnt From**
[Paella-Labs/crossbit-main#21014](https://github.com/Paella-Labs/crossbit-main/pull/21014)

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: address review ..."

Comment on lines +35 to +37
function redactValue(_value: unknown): string {
return "[REDACTED]";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Unused _value parameter in redactValue

The _value parameter is prefixed with _ to suppress the "unused variable" lint warning, but it's never read. This suggests the function was originally intended to do partial masking (as the PR description discusses) and was then simplified to always return [REDACTED]. The parameter can be removed entirely to make the intent clear.

Suggested change
function redactValue(_value: unknown): string {
return "[REDACTED]";
}
function redactValue(): string {
return "[REDACTED]";
}

The call sites would need to be updated too:

result[key] = redactValue();
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/utils.ts
Line: 35-37

Comment:
**Unused `_value` parameter in `redactValue`**

The `_value` parameter is prefixed with `_` to suppress the "unused variable" lint warning, but it's never read. This suggests the function was originally intended to do partial masking (as the PR description discusses) and was then simplified to always return `[REDACTED]`. The parameter can be removed entirely to make the intent clear.

```suggestion
function redactValue(): string {
    return "[REDACTED]";
}
```

The call sites would need to be updated too:
```ts
result[key] = redactValue();
```

How can I resolve this? If you propose a fix, please make it concise.

if (typeof value === "string" && value.length > 8) {
return `${value.slice(0, 4)}...${value.slice(-4)}`;
}
function redactValue(_value: unknown): string {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@devinai we want the previous behaviour

Copy link
Contributor

Choose a reason for hiding this comment

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

Restored the previous redactValue behavior in caa2e6e — strings >8 chars show first/last 4 chars (${value.slice(0, 4)}...${value.slice(-4)}), shorter values get [REDACTED].

Co-Authored-By: Alberto Elias <alberto.elias@paella.dev>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/SdkLogger.ts
Line: 416-422

Comment:
**Log message string not covered by redaction**

`redactSensitiveFields` is applied to the merged context object, but the `serializedMessage` string is passed to log sinks as-is. If a caller interpolates a sensitive value directly into the message (e.g. `logger.info(\`Authenticated with token ${jwt}\`)`), it will appear unredacted in the final `LogEntry.message`.

This is a known limitation of key-based redaction, but it is worth documenting explicitly so consumers know the contract: sensitive values must go into the context object (where they will be redacted by key), not into the message string.

Consider adding a note to the `log` method or the public `info`/`debug`/`error` API comments clarifying that message strings are **not** scanned for sensitive patterns.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: restore partial..."

@albertoelias-crossmint albertoelias-crossmint merged commit 24ea012 into wallets-v1 Mar 20, 2026
2 checks passed
@albertoelias-crossmint albertoelias-crossmint deleted the devin/1773964118-wal-9455-redact-sensitive-logs branch March 20, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants