Reviewing a Security-Critical Change

The interviewer hands you a diff for a service that moves money and asks “what's missing?” The senior answer is a checklist you run, not a vibe you have.

This is the depth question in the code-review track. You're handed a pull request for a microservice that handles secure transactions — a payments path, a consent write, a credential exchange — and asked what a senior reviewer would flag. The trap is to comment on style and a missing null check and stop. The signal is that you run the change through a repeatable security-and-correctness checklist, name the classes of risk that would cause a SEV or a breach, and then describe how you'd deliver that feedback so it lands and the diff still ships. What follows is the checklist a senior reviewer runs, plus a wrong-versus-right snippet, plus how to give the feedback.

The security-critical review checklist: tests and failure paths, error handling, concurrency, secrets, authn/authz, input validation, idempotency, encryption, no PII in logs and an audit trail, rate limiting, dependencies, safe rollout
The checklist a senior reviewer runs on a security-critical change: missing tests on the failure paths, unhandled errors, concurrency on shared state, secrets handling, authn/authz, input validation, idempotency for money movement, encryption, PII in logs plus an audit trail, rate limiting, supply-chain risk, and a safe rollout.

What this question is really testing

Whether you have an internalized threat model for code that touches money, secrets, or user data — and the discipline to apply it the same way every time rather than relying on catching something clever in the moment. The interviewer wants to hear that you review for the classes of failure that hurt (a breach, a double-charge, a leaked secret, a data-corruption SEV), that you weight the failure and error paths as heavily as the happy path, and that you can prioritize: what actually blocks the land versus what's a follow-up. They're also listening for whether you'd deliver a security block without shaming the author, because a reviewer who's feared gets routed around.

How to answer What the interviewer is looking for

The checklist a senior reviewer runs

Group your findings the way you'd group them in the review so the author can act on them. Everything above the fold blocks a land; the operational hardening is where you separate must-fix from fast-follow.

AreaWhat you look forWhy it’s here
Tests & failure pathsUnit and integration tests that exercise the error branches, retries, and partial failures — not just the happy pathThe bugs that hurt live in the paths no one tested
Error handlingNo unhandled exceptions; no bare except; errors surfaced, not swallowed; resources released on the failure pathA swallowed error in a money path is a silent data bug
ConcurrencyRaces on shared mutable state; check-then-act on balances; correct locking or an atomic compare-and-setTwo concurrent requests must not both spend the same funds
Secrets & credentialsNo hardcoded secrets or keys; pull from a secrets manager; least-privilege scopes; rotation possibleA committed secret is a breach waiting in git history
AuthN / AuthZCaller is authenticated and authorized for this resource; no missing object-level checks (IDOR)Authn without per-object authz is the classic escalation
Input validation & injectionValidate and bound all external input; parameterized queries; no shell string-building; no unsafe deserializationSQL, command, and deserialization injection all start here
IdempotencyMoney movement carries an idempotency key; a retry or duplicate delivery cannot double-applyAt-least-once delivery means retries will happen
EncryptionTLS in transit; sensitive fields encrypted at rest; correct algorithms, no home-rolled cryptoTransactions and PII must be protected on the wire and on disk
PII in logs & auditNo card numbers, tokens, or PII in logs or error messages; a tamper-evident audit trail of who did whatLogs leak, and money movement needs a forensic record
Rate limiting & abusePer-caller limits, bounded retries with backoff, and abuse/fraud guards on the endpointAn unbounded money endpoint is a fraud and DoS surface
DependenciesNew libraries vetted, pinned, and scanned; no unmaintained or typo-squatted packagesSupply-chain risk enters through a one-line import
Safe rolloutBehind a gate/flag; canary or staged ramp; a fast, tested rollbackYou want to turn a bad change off in minutes, not redeploy

Wrong versus right

The two anti-patterns you flag most often on a transaction path are a hardcoded credential and an unchecked, non-idempotent charge. Here's what you comment on, and the fix you offer alongside the block.

What you'd block
# WRONG: hardcoded secret, unchecked exception, no idempotency
API_KEY = "sk_live_9f3a2b7c1d4e"  # secret in source → leaks via git history

def charge(user_id, amount):
    conn = payments.connect(API_KEY)
    # SQL built by string interpolation → injection
    row = conn.query("SELECT balance FROM acct WHERE id = " + user_id)
    # check-then-act race + no retry guard → double-charge
    if row["balance"] >= amount:
        conn.debit(user_id, amount)   # raises on timeout; nobody catches it
    logging.info("charged %s for %s card=%s", user_id, amount, row["card"])
    # ^ PII (card) in logs; no audit record; no authz check on user_id
What you'd approve
# RIGHT: secrets manager, handled errors, idempotent, authorized, no PII
def charge(caller, user_id, amount, idempotency_key):
    if not authz.can_debit(caller, user_id):        # per-object authz
        raise PermissionError("not authorized for account")

    api_key = secrets_manager.get("payments/api_key")  # least-privilege, rotatable
    try:
        with payments.connect(api_key) as conn:
            # parameterized query + atomic conditional debit → no race
            result = conn.debit_if_sufficient(
                account_id=user_id, amount=amount,
                idempotency_key=idempotency_key,      # retry-safe
            )
    except PaymentTimeout as err:
        audit.record("charge_timeout", user_id, amount)   # forensic trail
        raise ChargeFailed("payment provider timeout") from err  # handled + chained

    audit.record("charge_ok", user_id, amount)
    logging.info("charged account", extra={"user_id": user_id, "amount": amount})
    # ^ no card / PII in logs; structured context only
    return result

Common follow-ups

You have limited time — what do you check first?

How to answer

How do you review the parts you're not a security expert in?

How to answer

The author says the missing tests will come in a follow-up diff. Do you block?

How to answer

How do you deliver a hard security block without alienating the author?

How to answer
Where to get your data (Meta)