Skip to content

4.9 Review Secrets, Defaults, and Dangerous APIs

4.9 - Review Secrets, Defaults, and Dangerous APIs

Overview

These patterns are foot-guns and misplaced trust: secrets in source, obsolete APIs, dangerous primitives, weak framework defaults, and validation that exists only in the browser. Hunt for them as inventory passes, then prove impact.

The points below are the ideas this family chapter uses again and again.

  1. Secrets in source, dangerous dynamic APIs, weak defaults, and client-only validation expand blast radius.
  2. Prefer secret stores, safe APIs, and secure framework defaults.
  3. Client validation is UX; server enforcement is the control.
  4. Related variants share misplaced trust in code, config, or the browser.
  5. Evidence names the secret or API, missing control, impact, and a proving test.

After reading this chapter, we should be able to find hardcoded secrets, dangerous APIs, weak defaults, and client-only validation with shared habits.

Shared Review Model

Across every variant below, keep the same evidence habit: name the source, the sink, the missing control, the impact, and a test that would prove a fix.

Variants in This Family

Hardcoded secrets

Hardcoded secrets are credentials or cryptographic material stored in source code instead of a secret manager or environment configuration. Anyone with repository access—or an attacker who extracts strings from a binary or container image—may recover them. Hardcoded authentication bypasses are especially dangerous because they often survive code review as "temporary" test hooks.

Where to look

Signal Where to look
Feature type Login handlers, API gateways, third-party SDK init, mobile app config, CI/CD scripts
Input entry Password verification, header API-key checks, webhook signature validation
Static fields const, private static final, module-level variables, .env.example with real values
Bypass branches if (password.equals("...")) alongside bcrypt checks, debug flags in production paths
Crypto material AES keys, HMAC secrets, JWT signing keys, PEM blocks committed to git
Connection strings JDBC, MongoDB, Redis, SMTP URLs with embedded usernames and passwords
Client exposure JavaScript bundles, mobile source, public config endpoints shipping server keys

Appendix detail: Hardcoded secrets code reference.

Dangerous functions

Some language and platform APIs execute arbitrary code or commands when passed a string. eval() in JavaScript and Python, Function() constructors, script engines, and shell invocation with concatenated arguments are common examples. When attacker-controlled data reaches these functions, the impact is often full code execution in the application's process context.

Where to look

Signal Where to look
Feature type Formula evaluators, rule engines, admin scripting, plugin loaders, template logic
Dynamic execution eval, exec, compile, Function(, setTimeout with string arguments
Shell invocation subprocess with shell=True, /bin/sh -c, Runtime.exec with concatenated commands
Reflection Class.forName, importlib, user-controlled method or class names at runtime
Plugin loading JARs, .so files, or scripts loaded from user-upload paths
Template logic Jinja2 unsafe extensions, Velocity user templates, server-side scriptlets
Deserialization overlap Native object deserialization treated as dynamic instantiation (see deserialization chapter)

Appendix detail: Dangerous functions code reference.

Obsolete code

Dead code, test hooks, and outdated modules often linger after refactors. Some paths are still reachable through direct URLs, old API versions, or feature toggles left enabled. Obsolete authentication helpers, deprecated encryption routines, and temporary admin endpoints may lack the hardening applied to newer code. Attackers probe forgotten routes; insiders may know URLs that documentation no longer mentions.

Where to look

Signal Where to look
Feature type Legacy login routes, old API versions (/v0/, /v1/), debug endpoints, profiling handlers
Test artifacts Hardcoded users, mock payment flows, QA bypasses without environment gating
Feature flags Toggles defaulting to on, experiments never removed, alternate upload paths
Deprecated handlers @Deprecated controllers, duplicate login implementations, pre-refactor validation
Commented blocks Disabled auth checks left in comments, large duplicated logic with weaker controls
Build gaps Debug controllers in Release builds, test packages bundled into production JARs
Static analysis hits Unreferenced methods, unreachable branches flagged by coverage or linters

Appendix detail: Obsolete code code reference.

Framework secure defaults

Secure by default means the standard configuration is safe without extra steps. Web frameworks often provide CSRF middleware, template auto-escaping, secure session cookies, and parameterized data access. Teams can undermine these benefits with DEBUG=True, disabled CSRF, raw template modes, or secrets in source control.

Where to look

Signal Where to look
Config surfaces settings.py, application.yml, appsettings.json, config/environments/*.rb, Express app.js
Debug in prod DEBUG=True, app.debug, verbose stack traces, Werkzeug debugger exposed
CSRF disabled @csrf_exempt, csrf().disable(), missing CSRF on cookie-auth forms
Raw output \|safe, th:utext, @Html.Raw on user-controlled model fields
Cookie flags Missing Secure, HttpOnly, SameSite on session identifiers
Hardcoded secrets SECRET_KEY, JWT secrets, DB passwords committed in config files

Appendix detail: Framework secure defaults code reference.

Client-side validation

Missing server-side validation is a business logic and input-trust flaw. Browsers can enforce pattern, required, maxlength, and JavaScript checks for user experience. Attackers bypass these controls with modified requests, custom HTTP clients, or browser devtools.

Where to look

Signal Where to look
Feature type Checkout, registration, transfers, profile update, admin forms, SPA JSON APIs
Client-only guards HTML5 pattern, min, max, required; React/Vue validators with no server mirror
Hidden/trusted fields role, price, userId, discount in POST bodies accepted without server recomputation
API parity gaps Mobile and third-party callers hit endpoints with weaker validation than the web UI
Missing server libs Handlers with no Bean Validation, Pydantic, FluentValidation, or Go validator tags
Partial persistence Invalid input rejected in UI but partially saved when API calls skip validation

Appendix detail: Client-side validation code reference.

Insecure coding practice

These flaws come from convenience shortcuts, copy-pasted samples, or framework defaults left unchanged. The application still “uses HTTPS” or “uses JWT,” but the code does not actually validate the peer, token, or cookie the way the design assumes.

Where to look

Pattern Where to look Red flags
TLS / HTTPS verification disabled requests.get(..., verify=False), custom TrustManager that accepts all certs, NODE_TLS_REJECT_UNAUTHORIZED=0, gRPC/HTTP clients with insecure channel creds Comments like “fix cert later,” test code shipped to prod
JWT signature / key mishandling get_unverified_claims, hardcoded HMAC secrets, accepting alg: none, JWKS without issuer/audience checks Auth middleware that trusts base64 payload segments
Insecure HTTP cookie flags set_cookie without httponly/secure/samesite, legacy servlet cookies, framework session defaults Session ID in URL, year-long Max-Age, logout that only clears client cookie
Hardcoded secrets (related) API keys, DB passwords, signing keys in source See 4.9 § Hardcoded secrets
Weak or custom crypto (related) MD5 passwords, home-grown AES, mixed hash/encrypt See 4.8 Review Cryptography in Application Code § Cryptographic implementation, 4.8 Review Cryptography in Application Code § Non-standard crypto practices, 4.8 Review Cryptography in Application Code § Encryption and decryption mistakes
Dangerous dynamic execution (related) eval, exec, script engines on user input See 4.9 § Dangerous functions
Sensitive data in logs or URLs (related) Tokens/passwords in logs, credentials in GET See 4.7 Review Information Disclosure and Logging § Sensitive data in URL, 4.7 Review Information Disclosure and Logging § Sensitive logging
Secrets in comments (related) Password hints in HTML/JS comments See 4.7 § Sensitive code comments
Insecure deserialization (related) pickle.loads on untrusted bytes See 4.3 § Insecure deserialization
Framework defaults left weak (related) DEBUG=True, permissive CORS, CSRF off See 4.9 § Framework secure defaults

Suggested additions for the same review pass: debug endpoints left enabled in production, permissive CORS with credentials, missing CSRF on state-changing cookie auth (4.5 Review Authentication, Session, and Access Control § CSRF), trust-all proxy headers without validation, and disabling security headers (CSP, HSTS) at the edge.

Appendix detail: Insecure coding practice code reference.

Worked Example (Hardcoded secrets)

We walk Hardcoded secrets in depth. Apply the same tracing steps to the other variants, adjusting sources and sinks from the tables above.

Sample vulnerable code (Python)

AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

@app.route("/admin/sync")
def admin_sync():
    api_key = request.headers.get("X-API-Key")
    # Hardcoded bypass key grants admin access without user authentication
    if api_key == "nightly-etl-bypass-7k9m":
        return admin_dashboard()
    return "", 403

def sign_token(payload: dict) -> str:
    # JWT signing key embedded in source — cannot rotate without redeploy
    return jwt.encode(payload, "prod-jwt-hmac-static-v3", algorithm="HS256")

Step-by-step review walkthrough

  1. Search authentication checks. Look for equals, ==, or compare against user-supplied passwords or API keys with string literals on either side.
  2. Inspect constants and config modules. Read config.py, .env files in git, and settings.py for API keys, connection strings, and OAuth client secrets.
  3. Review bypass branches. Flag OR conditions on password verification that accept a fixed string alongside normal hash checks.
  4. Trace encryption and signing. Follow JWT, HMAC, and field-encryption code for hardcoded keys, IVs, and salts instead of key-management services.
  5. Check client-side code. Mobile apps and front-end bundles must not ship server secrets; only public identifiers belong in client code.
  6. Follow third-party SDK initialization. Stripe, Twilio, and cloud SDK calls often hide inline keys in setup blocks.
  7. Confirm CI/CD injects secrets at runtime. Dockerfiles, Helm charts, and Terraform in git should reference secret names, not literal values.

Risk Impact (Family)

Authentication bypass. Hardcoded backdoor passwords or API keys let attackers skip normal credential checks entirely.

Credential theft and replay. Keys in source propagate to forks, backups, and decompiled binaries; stolen keys work until manual rotation.

Cloud and data exposure. Embedded AWS or database credentials may grant broad infrastructure access beyond the application tier.

Compliance and audit failure. Regulators and customers expect secrets in vaults with rotation logs; hardcoded values fail basic control reviews.

Incident response delay. When a key leaks, teams must redeploy code instead of rotating a secret in a manager within minutes.

Fix Principles

Primary safer patterns for the worked example follow. For other languages and variant-specific fixes, use the family appendix.

Python

Load secrets from environment or a secret backend. Remove test bypasses from production paths.

import os
import jwt

def get_secret(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"Missing required secret: {name}")
    return value

@app.route("/admin/sync")
def admin_sync():
    expected = get_secret("ADMIN_SYNC_API_KEY")
    if secrets.compare_digest(request.headers.get("X-API-Key", ""), expected):
        return admin_dashboard()
    return "", 403

def sign_token(payload: dict) -> str:
    key = get_secret("JWT_SIGNING_KEY")
    return jwt.encode(payload, key, algorithm="HS256")

Use pydantic-settings or django-environ for typed configuration. Run gitleaks or GitHub secret scanning in CI.

Verify During Review

Use the checklist below as a family-level pass over the variants above. Each item should map to evidence in the change under review.

  • No passwords, API keys, tokens, or private keys appear as string literals in application code.
  • Authentication logic has no hardcoded bypass branches alongside normal credential checks.
  • Secrets load from environment variables, secret managers, or encrypted configuration at runtime.
  • Front-end and mobile clients use public identifiers only; sensitive keys stay on the server.
  • Secret scanning runs in CI and on historical commits when onboarding a repository.
  • Rotation procedures exist for every secret class; hardcoded values cannot be rotated without redeploying code.
  • User-controlled input does not reach eval, equivalent dynamic execution, or unsafe deserialization APIs.
  • Shell commands use argument arrays and allowlists; no shell=True or sh -c with interpolated user data.
  • Plugin and template features use restricted DSLs or admin-only authoring with code review, not open user scripting.
  • Dangerous functions in legacy modules are scheduled for removal or isolated from production routes.
  • Static analysis and security tests cover known dangerous API usage in the codebase.
  • Dead code, duplicate legacy endpoints, and test-only routes are removed or strictly environment-gated.
  • Feature flags that expose alternate code paths have owners, expiry dates, and production defaults that fail secure.
  • Deprecated API versions receive the same authentication, authorization, and input validation as current versions—or are shut down.
  • Production builds exclude debug, profiling, and administrative utilities not required in prod.
  • Coverage or static analysis confirms unreachable security-sensitive code is deleted, not commented out.
  • Technical debt tickets for obsolete security paths are prioritized alongside new feature work.
  • Framework secure defaults are enabled and not overridden in production configuration.
  • Debug, verbose errors, and permissive CORS are confined to local development.
  • Session and CSRF protections match the authentication model (cookie sessions need CSRF).
  • Template and API output paths use framework escaping unless a documented exception exists.
  • Dependencies are current and monitored; configuration drift is reviewed in each release.
  • Every user-editable field has equivalent server-side validation before business logic runs.
  • HTML5, JavaScript, and mobile validations are treated as UX only, not security controls.
  • Trusted values (price, role, user ID, discount eligibility) are computed server-side, not read from the client.
  • Invalid input returns consistent 400 responses with safe error messages; handlers do not partially persist bad data.
  • API endpoints used by SPAs and mobile apps enforce the same rules as server-rendered forms.
  • Security tests bypass the front end and send out-of-range, missing, and malformed fields to each endpoint.
  • No production code path uses verify=False, trust-all TLS callbacks, or equivalent.
  • JWT validation enforces signature, allowed algorithms, exp, and iss/aud where applicable; no verify_signature=False in deployed branches.
  • Signing keys load from secret stores or JWKS with rotation; no long-lived hardcoded symmetric secrets in source.
  • Session cookies use HttpOnly + Secure + deliberate SameSite; lifetimes match policy.
  • User-controlled URLs are not fetched with TLS verification disabled (pair with SSRF allowlists).
  • Related chapters (hardcoded secrets, CSRF, session management) are checked when any finding above is present.

Code Reference (Appendix)

Payloads, language-specific sinks, multi-language examples, and full fix catalogs for every variant live in 4.9 code reference — Review Secrets, Defaults, and Dangerous APIs.

Reference