Mask Sensitive API Fields

Learn to mask sensitive fields in API responses to prevent data leaks. This lesson covers what to mask, practical steps, and troubleshooting.

Focus: mask sensitive fields in api responses

Sponsored

You've built the perfect API endpoint — fast, well-documented, tested. Then someone points out that the GET /users/me response contains the user's password_hash, credit_card.cvv, and api_key. Your heart sinks. These fields should never leave your server, but there they are, sitting in a JSON payload that could leak through logs, browser history, or a monitoring dashboard. In this lesson, you'll learn exactly how to mask sensitive fields in API responses — not just by deleting them, but by controlling what each consumer sees, so you can ship safely without breaking integrations.

The problem this lesson solves

APIs are the front door to your data. Every response you send is a potential leak. Sensitive data like password hashes, tokens, and credit card numbers often sneak into responses because developers serialize entire database models, or they add a field to a model and forget it's exposed. The result is a data breach waiting to happen — and regulators like GDPR and PCI-DSS are not forgiving.

The core pain: You don't need to block all data, you need to selectively hide fields from responses while still allowing internal services to see what they need. Removing fields entirely can break legacy clients, and simply encrypting them doesn't solve the problem if the key is in the same app. Masking lets you show a placeholder (like *** or ••••1234) so the shape of the data stays intact but the secret is gone.

This lesson teaches you how to implement masking from scratch — with coroutines, decorators, and libraries — so you can protect data today without a heavy framework.

Core concept / mental model

Think of an API response as a watermelon: juicy, full of useful data. But some seeds (sensitive fields) are toxic if swallowed. Masking is like wrapping the seeds in a paper wrapper: the fruit is still there, the seeds are visible but not edible.

Formally, masking means replacing a sensitive value with a placeholder that retains the original's data type and length. For example:

  • password: "hunter2""****"
  • card_number: "4111111111111111""4111111111"
  • email: "alice@example.com""a***@example.com"

Masking is different from redaction (removing the field entirely), encryption (reversible), and hashing (irreversible but deterministic). Masking is the only technique that preserves the response schema, so clients that only check field existence keep working.

Mental model: You are building a filter pipeline. The incoming data flows through a series of maskers — each knows which fields to hide and how to hide them. The pipeline runs at serialization time, so you can have different masks for different endpoints (e.g., admins see full data, normal users see masked).

How it works step by step

  1. Define the sensitive fields list — explicitly enumerate field names that must never appear in plaintext. Do it in one place.
  2. Choose a masking strategy — full mask (****), partial mask (keep last 4), or deterministic mask (same input → same output).
  3. Apply masks at the serialization layer — before the response is sent, not after (too late!).
  4. Respect context — allow admins or internal services to see real data via a flag or token.
  5. Test thoroughly — ensure no leaks in error messages, logs, or nested objects.

In Python, you can implement masking with a coroutine-based chain (like middleware), or a simple function that walks through a dictionary recursively. The step-by-step logic is: parse the response dict → find keys that match sensitive patterns → replace values → return the masked dict.

Hands-on walkthrough

Example 1: Basic mask function for flat JSON

import re

SENSITIVE_FIELDS = {"password", "password_hash", "api_key", "cvv"}

def mask_flat(data: dict) -> dict:
    masked = {}
    for key, value in data.items():
        if key in SENSITIVE_FIELDS:
            masked[key] = "********" if value else None
        else:
            masked[key] = value
    return masked

user = {"id": 1, "username": "alice", "password_hash": "abc123"}
print(mask_flat(user))
# Output: {'id': 1, 'username': 'alice', 'password_hash': '********'}

Example 2: Recursive masking for nested structures

import json

SENSITIVE_KEYS = {"cvv", "pin", "token"}

def mask_recursive(data):
    if isinstance(data, dict):
        return {k: (mask_recursive(v) if k not in SENSITIVE_KEYS else "****") for k, v in data.items()}
    elif isinstance(data, list):
        return [mask_recursive(item) for item in data]
    else:
        return data

payload = {"card": {"number": "4111111111111111", "cvv": "123"}, "items": [{"token": "secret"}]}
print(json.dumps(mask_recursive(payload)))
# Output: {"card": {"number": "4111111111111111", "cvv": "****"}, "items": [{"token": "****"}]}

Example 3: Partial masking (keep last 4)

def mask_partial(value: str, keep_left: int = 4, keep_right: int = 4, mask_char: str = "*") -> str:
    if not value or len(value) <= keep_left + keep_right:
        return mask_char * len(value)
    return value[:keep_left] + mask_char * (len(value) - keep_left - keep_right) + value[-keep_right:]

card = "4111111111111111"
print(mask_partial(card))  # Output: 4111********1111

Example 4: Use with FastAPI (coroutine-based wrapper)

from fastapi import FastAPI, Depends
from pydantic import BaseModel

app = FastAPI()

class UserOut(BaseModel):
    id: int
    username: str
    password: str = "********"  # but this doesn't intercept input!

@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
    # Simulate a database record with a real hash
    db_user = {"id": user_id, "username": "alice", "password": "$2b$12$secret"}
    # Manual masking before response
    db_user["password"] = "********"
    return db_user

Pro tip: In FastAPI, you can also override response_class or use a dependency to mask globally. But the above shows the simplest way — mask at the endpoint boundary.

Compare options / when to choose what

Technique Preserves schema? Reversible? Use case
Masking (full) Yes No Public APIs, show placeholder
Redaction (remove) No No Internal microservices don't need field
Encryption Yes (cipher) Yes (with key) At-rest storage, not for HTTP responses
Hashing (e.g., SHA-256) Yes (hash) No User identifiers, but not human-readable

When to choose what: - Masking for any field that must appear but not be readable (credit card, tokens). - Redaction when you don't want to reveal even the field's existence (e.g., hidden internal flags). - Encryption when you need to send the data to a trusted service that can decrypt (not recommended over HTTP without TLS). - Hashing for identifiers that need deterministic matching (like username lookup).

Variations: You can use libraries like python-masker, json-masker, or Pydantic's Field(exclude=True) for redaction. Or write your own coroutine chain for full control.

Troubleshooting & edge cases

Common mistakes and how to fix them:

  • Mistake: Masking at the view layer only, but error logs still print the original data. Fix: Mask in the serialization layer and also configure logging filters.
  • Mistake: Forgetting nested fields — you mask credit_card but not credit_card.cvv. Fix: Use recursive masking.
  • Mistake: Masking everything including non-sensitive fields, breaking clients. Fix: Explicitly list sensitive fields, not a blacklist of everything.
  • Mistake: Using a masking function that accidentally reveals length (e.g., len(password) leaks info). Fix: Mask to a fixed length for variable-length fields.

Edge cases: - Empty values: Should you mask None? Decide: None often indicates no data, so masking it as "********" might confuse. Better to return None. - Binary data: If a field is bytes, your mask function should handle bytes type separately. - Performance: Recursive masking on large payloads can be slow. Pre-compile your sensitive key list as a set for O(1) lookup. - Logs: Even if you mask the response, your app logs might include the request body. Always mask before logging.

What you learned & what's next

You now know how to mask sensitive fields in API responses — from flat to nested structures, with full or partial masking, and how to choose between masking, redaction, encryption, and hashing. You practiced with coroutine-based examples and learned to avoid common pitfalls like leaks in logs and nested key misses.

Next lesson in the Secure development track: Secure response headers and CORS configuration — because masking helps with data exposure, but headers control what browsers and external sites can do with your response. Get ready to lock down your API layer even further.

Practice recap

Write a FastAPI endpoint that returns a user object with a nested payment dict containing card_number, cvv, and expiry. Use a recursive mask function that turns cvv into *** and applies partial masking to the card number (keep first 4 and last 4). Verify that the response JSON does not contain the full CVV or card number in any field, including logs.

Common mistakes

  • Masking only at the view level while error logs still print the original sensitive data. Always apply masks in the serialization layer and configure logging filters.
  • Forgetting to mask nested fields — e.g., masking credit_card but leaving credit_card.cvv exposed. Use recursive masking to handle deep structures.
  • Masking every field, breaking clients that rely on field names or data types. Maintain an explicit allowlist of non-sensitive fields instead of blacklisting everything.
  • Revealing data length by masking with a fixed-length string for variable-length secrets like passwords. Use a consistent mask length to avoid leaking info.

Variations

  1. Use Pydantic Field(exclude=True) to redact fields in FastAPI responses, but note that this removes the field entirely rather than masking it.
  2. Employ library-based solutions like python-masker or json-masker that automatically discover and mask keys by name patterns across nested JSON.
  3. Implement a coroutine-based masking middleware (e.g., in aiohttp or ASGI) to intercept all responses and apply masks uniformly without touching each endpoint.

Real-world use cases

  • E-commerce API that returns order details — mask credit card numbers and CVV while keeping the last four digits for display.
  • User management microservice that serializes user models — mask password_hash and api_key before sending responses to the frontend.
  • Healthcare data API returning patient records — mask social security and insurance IDs, showing only a partial identifier for staff verification.

Key takeaways

  • Masking preserves response schema by replacing sensitive values with placeholders, unlike redaction or encryption.
  • Always define a centralized list of sensitive fields and apply masks recursively to cover nested objects.
  • Choose between full mask, partial mask, or deterministic mask based on the need to protect versus usability.
  • Mask at the serialization layer, not after the response is built, to avoid leaks in logs and error messages.
  • Consider context: allow admins or internal services to see full data via a flag or token while masking for public consumers.
  • Test with nested structures, empty values, and binary data to ensure your mask function behaves correctly.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.