Skip to content

fix(security): bound exception telemetry without raw traceback - #1700

Draft
seonghobae wants to merge 5 commits into
codex/sentinel-exception-logging-successorfrom
fix/structured-exception-telemetry
Draft

seonghobae wants to merge 5 commits into
codex/sentinel-exception-logging-successorfrom
fix/structured-exception-telemetry

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Authority

Verified RED

#1612 correctly stopped rendering raw exception values on its bounded security surface, but its shared redacted_exception_info() retained the original traceback. Python logging can therefore still render internal source locations. #1698 requires a sink-aware structured correlation contract rather than treating tracebacks as redaction.

The first #1700 head removed raw traceback rendering and introduced a deterministic fingerprint, but the key was only exception type + module + function. Two independent exception statements inside one function therefore collapsed into the same incident signature. That weakens RCA and can hide a new causal path behind an existing noisy one.

Protected develop still contains additional exc_info=True and raw exception-interpolation sinks outside #1612's bounded surface. This PR does not blanket-rewrite those sites or claim #1698 complete.

Minimal causal repair

The #1612 caller API remains unchanged. core.safe_logging.redacted_exception_info(error) now:

  • never renders str(error) / repr(error);
  • never attaches the original traceback to the LogRecord;
  • emits the exception class name plus a 16-hex SHA-256 correlation fingerprint;
  • derives the fingerprint from qualified exception type plus the exact deepest execution site: module, qualified function, traceback line, and bytecode offset;
  • hashes those location components before logging; raw path/function/line/offset values are not emitted;
  • preserves message-independent correlation for one unchanged failure site while distinguishing separate failure statements in the same function;
  • handles unraised/no-traceback exceptions with the same bounded contract.

Fingerprint stability is intentionally build-scoped: source edits or interpreter/compiler changes may change line/bytecode identity and therefore the fingerprint. This is preferable to falsely merging distinct failure sites.

Verification contract

backend/tests/test_safe_logging.py drives the real Python logging.Formatter with token-like values and a PostgreSQL connection string. It requires:

  • secret text absent from rendered output;
  • source path/function absent from rendered output;
  • exception class retained;
  • 16-hex correlation fingerprint retained;
  • the same failure site to keep the same fingerprint across different secret-bearing messages;
  • two different raise statements in the same function to produce different fingerprints without rendering either source location;
  • no-traceback exceptions to remain bounded.

docs/doctoring/exception-telemetry-confidentiality.md records the decision, rejected module/function-only alternative, build-scoped stability boundary, verification, CWE-532 / OWASP traceability, and why #1698 remains open for sink-by-sink migration.

Independent review

CodeRabbit reviewed exactly 3da3ae8e60e1bb049f59ae86bfe82db12b7e3cc7 → 2d6800aea110ac04eeedcd5faa91479f62877408, selected all three effective files, generated no actionable comment, and submitted formal APPROVED review PRR_kwDOSNjZ2s8AAAABNs7L5g at 2026-09-15T18:52:19Z. This is current-head review evidence; it does not substitute for hosted required checks or protected-lineage integration.

Evidence boundary

Exact 2d6800aea110ac04eeedcd5faa91479f62877408 has zero PR-triggered repository workflow runs because this PR is stacked on #1612 and the repository-local stacked-PR trigger defect is still owned by #1691. Do not manufacture checks through temporary retargeting, dummy/no-op commits, copied workflows, synthetic statuses, or predecessor receipt transfer.

Stack boundary

Do not merge this PR into #1612 while #1612's exact-head evidence is still the active parent gate, because doing so would move the parent head and invalidate that evidence. After #1612 normally integrates to protected develop, reconcile/retarget this effective delta onto the integrated owner lineage and regenerate hosted evidence. No force push, destructive rebase, self-approval, check suppression, no-op requeue commit, or gate weakening.

Gate

Keep Draft until the unchanged final integrated head has terminal success for every then-live required context, no valid unresolved current-head finding/thread, and qualifying post-last-push independent review. Issue #1698 remains open until the remaining production sinks and their trust/retention boundaries are inventoried and migrated with source-backed tests.

Summary by CodeRabbit

  • Bug Fixes

    • Improved exception logging confidentiality by preventing original traceback details and sensitive exception messages from appearing in redacted output.
    • Redacted logs now include the exception type, a standardized redaction notice, and a bounded fingerprint for distinguishing failure locations.
    • Unraised exceptions are handled consistently without exposing traceback data.
  • Documentation

    • Added guidance describing exception telemetry confidentiality boundaries and the information retained in redacted logs.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 5247d1d2-f335-45d7-bd16-e66ebc6810fe

📥 Commits

Reviewing files that changed from the base of the PR and between 3da3ae8 and 2d6800a.

📒 Files selected for processing (3)
  • backend/core/safe_logging.py
  • backend/tests/test_safe_logging.py
  • docs/doctoring/exception-telemetry-confidentiality.md

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The safe logging helper now returns a redacted exception message, no traceback object, and a location-based fingerprint. Tests verify secret exclusion, fingerprint behavior, and unraised exceptions. A design document records the confidentiality boundary.

Changes

Confidential exception telemetry

Layer / File(s) Summary
Redaction and fingerprint generation
backend/core/safe_logging.py
redacted_exception_info now returns no traceback and includes a 16-character SHA-256 fingerprint derived from the exception type and deepest failure location.
Verification and confidentiality contract
backend/tests/test_safe_logging.py, docs/doctoring/exception-telemetry-confidentiality.md
Tests verify secret exclusion, message-independent fingerprints, distinct failure-site fingerprints, and unraised exceptions. The design document records the telemetry boundary and rejected alternatives.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 2d680

The change removes raw exception values and tracebacks, with no verified issue currently preventing merge.

🚥 Pre-merge checks | ✅ 4
✅ 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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: limiting exception telemetry and removing raw tracebacks.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/structured-exception-telemetry

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.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact head 6b515f5b2e08241eeecd4b587a50a68a79f60c76. Focus on whether the fingerprint contract can leak exception value/traceback/location data, whether correlation remains stable enough for RCA, and whether the logging tuple behaves safely for both raised and unraised exceptions. Do not treat predecessor #1612 review evidence as current-head approval.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Rate Limit Exceeded

@seonghobae have exceeded the limit for the number of chat messages per hour. Please wait 3 minutes and 21 seconds before sending another message.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review exact head 2d6800aea110ac04eeedcd5faa91479f62877408. Please review the confidentiality boundary and the new same-function/different-failure-site fingerprint contract; predecessor review state is not transferable.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== HEAD =='
git rev-parse HEAD
printf '%s\n' '== status =='
git status --short
printf '%s\n' '== effective diff names =='
git diff --name-status 3da3ae8e60e1bb049f59ae86bfe82db12b7e3cc7..HEAD
printf '%s\n' '== relevant file candidates =='
fd -t f -e py . | rg '(^|/)(safe_logging|test_safe_logging)\.py$' || true
printf '%s\n' '== Python symbol outline =='
for f in $(fd -t f -e py . | rg '(^|/)(safe_logging|test_safe_logging)\.py$'); do
  echo "--- $f"
  ast-grep outline "$f" --items all --type function,class || true
done
printf '%s\n' '== effective diff =='
git diff --find-renames --find-copies --unified=80 3da3ae8e60e1bb049f59ae86bfe82db12b7e3cc7..HEAD -- \
  ':(glob)**/safe_logging.py' \
  ':(glob)**/test_safe_logging.py' \
  ':(glob)**/exception-telemetry-confidentiality.md'
printf '%s\n' '== call sites =='
rg -n -C 4 '\bredacted_exception_info\s*\(' --glob '*.py' . || true
printf '%s\n' '== logging format configuration =='
rg -n -C 3 '(format\s*=|Formatter\s*\(|basicConfig\s*\(|exc_info\s*=)' --glob '*.py' . | head -n 400 || true

Length of output: 41781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== safe_logging.py =='
cat -n backend/core/safe_logging.py
printf '%s\n' '== test_safe_logging.py =='
cat -n backend/tests/test_safe_logging.py
printf '%s\n' '== existing boundary test =='
cat -n backend/tests/test_exception_logging_boundaries.py
printf '%s\n' '== imports and packaging context =='
rg -n -C 2 'from core\.safe_logging import|import core\.safe_logging' --glob '*.py' backend
printf '%s\n' '== standard library formatter probe =='
python - <<'PY'
import logging

secret = "postgresql://user:password@internal.example/db"
exc_type = type(f"ProviderError_{secret}", (Exception,), {})
record = logging.LogRecord(
    name="probe",
    level=logging.ERROR,
    pathname="/internal/path.py",
    lineno=1,
    msg="operation failed",
    args=(),
    exc_info=(RuntimeError, RuntimeError(
        f"Exception details redacted exception_type={exc_type.__name__} "
        "exception_fingerprint=0123456789abcdef"
    ), None),
)
print(logging.Formatter("%(message)s").format(record))
PY
printf '%s\n' '== type name assignment probe =='
python - <<'PY'
class ProbeError(Exception):
    pass
ProbeError.__name__ = "token=super-secret-value"
print(ProbeError.__name__)
PY

Length of output: 23993


@seonghobae I reviewed exact head 2d6800aea110ac04eeedcd5faa91479f62877408 independently. I did not use predecessor review state.

I found two issues.

  1. backend/tests/test_exception_logging_boundaries.py Line 92 is now incompatible with the new contract.
    The test still requires _render_redacted_exception_log in the rendered output. redacted_exception_info() now returns None as the traceback, so Python logging will not render that frame name. The test will fail when it runs. Update the assertion to require that the function name and source path are absent. This also preserves the new confidentiality boundary.

  2. backend/core/safe_logging.py Line 51 can emit secret-derived text through type(exc).__name__.
    The helper directly writes the exception class name into safe_message. Python allows a dynamic exception type or a mutated __name__ to contain arbitrary text. A standard-library formatter probe emitted a PostgreSQL connection string placed in a dynamic exception class name. Therefore, the helper does not provide a bounded confidentiality contract for arbitrary exception instances.

    Use a trusted exception-type taxonomy, or replace the emitted class name with a fixed safe category plus an opaque type fingerprint. Add a regression test that creates an exception type whose __name__ contains token-like text and verifies that the rendered record does not contain that text.

The traceback suppression and deepest-site fingerprint design otherwise prevent the original exception value and traceback frames from reaching Python’s formatter. The same-function, different-raise-site test also covers the intended line and bytecode distinction.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review only exact current head 2d6800aea110ac04eeedcd5faa91479f62877408 against stacked base 3da3ae8e60e1bb049f59ae86bfe82db12b7e3cc7. Focus on whether the structured exception fingerprint preserves sufficient RCA correlation without leaking raw exception values or traceback/source locations, including same-site/different-message stability, distinct failure statements in one function, and no-traceback behavior. Predecessor review/check evidence does not transfer.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='2d6800aea110ac04eeedcd5faa91479f62877408'
base='3da3ae8e60e1bb049f59ae86bfe82db12b7e3cc7'

printf '%s\n' '== revision verification =='
git rev-parse HEAD
git cat-file -e "${head}^{commit}"
git cat-file -e "${base}^{commit}"
printf '%s\n' '== effective changed files =='
git diff --name-status "$base..$head"
printf '%s\n' '== candidate files =='
fd -t f . | rg '(^|/)(safe_logging\.py|test_safe_logging\.py|test_exception_logging_boundaries\.py|exception-telemetry-confidentiality\.md)$' || true
printf '%s\n' '== outlines =='
for f in backend/core/safe_logging.py backend/tests/test_safe_logging.py backend/tests/test_exception_logging_boundaries.py; do
  if [ -f "$f" ]; then
    echo "--- $f"
    ast-grep outline "$f" --items all --type function,class || true
  fi
done
printf '%s\n' '== effective diff =='
git diff --find-renames --unified=100 "$base..$head" -- \
  backend/core/safe_logging.py \
  backend/tests/test_safe_logging.py \
  backend/tests/test_exception_logging_boundaries.py \
  docs/doctoring/exception-telemetry-confidentiality.md
printf '%s\n' '== current helper =='
sed -n '1,240p' backend/core/safe_logging.py
printf '%s\n' '== current helper tests =='
sed -n '1,320p' backend/tests/test_safe_logging.py
printf '%s\n' '== boundary tests =='
sed -n '1,180p' backend/tests/test_exception_logging_boundaries.py
printf '%s\n' '== helper call sites =='
rg -n -C 5 '\bredacted_exception_info\s*\(' --glob '*.py' backend || true
printf '%s\n' '== logger formatter configurations =='
rg -n -C 3 '(logging\.Formatter\s*\(|Formatter\s*\(|format\s*=|exc_info\s*=)' --glob '*.py' backend | head -n 500 || true
printf '%s\n' '== exception name mutations and dynamic exception types =='
rg -n -C 3 '(__name__\s*=|type\s*\([^,]+,\s*\(\s*Exception|type\s*\([^,]+,\s*\(\s*BaseException)' --glob '*.py' backend || true

Length of output: 58455


@seonghobae I reviewed only exact head 2d6800aea110ac04eeedcd5faa91479f62877408 against stacked base 3da3ae8e60e1bb049f59ae86bfe82db12b7e3cc7. I did not use predecessor review or check evidence.

I found two issues.

  1. backend/tests/test_exception_logging_boundaries.py Line 92 will fail under the new contract.
    redacted_exception_info() now returns None for the traceback. Python logging therefore cannot render _render_redacted_exception_log. The test still requires that name in rendered. Change this assertion to require that the function name and source path are absent.

  2. backend/core/safe_logging.py Line 51 can disclose secret-derived data through type(exc).__name__.
    Python permits dynamically created exception classes and mutation of a class __name__. That value can contain a token or connection string. The helper writes it directly into safe_message, so the rendered log is not bounded for arbitrary exception instances. Emit a trusted fixed category, or an opaque type fingerprint, instead of the raw class name. Add a formatter regression test with a secret-bearing exception class name.

The synthetic RuntimeError and None traceback correctly prevent ordinary formatter output from including the original exception value or traceback location. The fingerprint input includes the deepest traceback line and bytecode offset. This preserves same-site, different-message correlation and distinguishes the two raise statements in one function. The unraised path also produces a bounded fingerprint.

Repository tests were not run. The review environment permits static inspection only.


✏️ Learnings added
Learnt from: seonghobae
URL: https://github.com/ContextualWisdomLab/naruon/pull/1700

Timestamp: 2026-09-15T18:44:03.010Z
Learning: For PR `#1700`, review evidence must bind to the exact current head. Review and check evidence from stacked predecessor PR `#1612` is not transferable as approval of a successor head.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

This branch has not been deployed

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

Labels

bug Something isn't working priority: high High-priority or P1 work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant