core/security/secure_coding.md

Secure Coding Practices Across Languages

Language-agnostic secure coding fundamentals: input validation, output encoding, secrets management, least privilege, dependency and SCA scanning, and secure-by-default design applicable across Python, JavaScript, Rust, Go, and other stacks.

Introduction / Overview

Secure coding is the practice of writing software that resists misuse by design, not by relying on a perimeter (firewall, WAF, network segmentation) to catch what the code itself should never allow. In 2026, with software supply chains, cloud-native deployments, and AI-assisted code generation all expanding the volume and speed of code shipped, secure-by-default habits at the code level are the highest-leverage control available to an individual engineer.

This document is deliberately language-agnostic: the principles (validate at the boundary, encode at the sink, never trust client input, least privilege everywhere) apply identically whether the implementation is Python, JavaScript/TypeScript, Rust, Go, or Java. For the specific, current catalog of web application risk categories these practices defend against, see OWASP Top 10.

When to use this guidance: Any code that accepts external input (HTTP requests, file uploads, message queue payloads, CLI arguments, environment configuration), handles credentials or secrets, or has a software supply chain (i.e., virtually all modern software).

Core Concepts

  • Trust boundary: The point where data crosses from an untrusted source (user input, third-party API, another service) into your system's control. All validation must happen at or before this boundary - never assume upstream validation was sufficient.
  • Validate on input, encode on output: These are two distinct operations often conflated. Validation rejects or normalizes data that does not conform to an expected shape; encoding transforms data for the specific context it will be rendered/interpreted in (HTML, SQL, shell, JSON) to neutralize its ability to be interpreted as code in that context.
  • Least privilege: Every component, service account, and process should hold the minimum permissions required to perform its function, and nothing more, minimizing the impact of any single compromise.
  • Secure defaults: A system's default configuration should be the safe one; developers should have to explicitly opt into weaker security, not the reverse.
  • Defense in depth: No single control is assumed sufficient. Input validation, parameterized queries, least-privilege database accounts, and output encoding are layered so that a failure in one layer does not automatically become an exploit.

Best Practices

1. Validate All External Input at the Trust Boundary

Rationale: Input validation is the single highest-leverage control against injection, deserialization, and logic-abuse vulnerabilities. Validation must happen server-side; client-side validation is a UX convenience only.

from pydantic import BaseModel, ConfigDict, Field

class TransferRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    source_account_id: int = Field(gt=0)
    destination_account_id: int = Field(gt=0)
    amount_cents: int = Field(gt=0, le=10_000_000)
    memo: str = Field(max_length=280, pattern=r"^[\w\s.,-]*$")

Why this works well in production:

  • extra="forbid" rejects unexpected fields outright rather than silently discarding them, closing off mass-assignment style attacks.
  • Explicit numeric bounds (gt, le) prevent both logic errors (negative transfers) and resource-exhaustion abuse (absurdly large values).

2. Encode Output for the Specific Sink It Targets

Rationale: The same string requires different encoding depending on where it is rendered - HTML body, HTML attribute, JavaScript context, SQL query, shell command, or URL. Applying the wrong encoding (or none) is the root cause of most injection classes (see OWASP Top 10 - Injection).

// Correct: framework auto-escapes on render (React, template engines with autoescape on)
const el = <p>{userSuppliedComment}</p>; // React escapes by default

// Dangerous: bypasses encoding entirely
const el = <div dangerouslySetInnerHTML={{ __html: userSuppliedComment }} />;

Why this works well in production: Modern frameworks (React, Django templates, Jinja2 with autoescape, Vue) encode by default; the security-critical discipline is recognizing and minimizing the explicit "raw"/"unsafe" escape hatches (dangerouslySetInnerHTML, {% autoescape off %}, v-html) and treating every use of one as requiring explicit sanitization review.

3. Never Hard-Code Secrets - Centralize Secrets Management

Rationale: Secrets committed to source control persist in history indefinitely (even after deletion) and are the most common root cause of credential-leak incidents.

# Wrong
DATABASE_PASSWORD = "hunter2"

# Right: load from environment, backed by a secrets manager in production
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_password: str
    model_config = {"env_file": ".env"}

settings = Settings()

Why this works well in production:

  • .env files are used only for local development and are excluded via .gitignore; production injects secrets via the platform's secret store (AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets backed by an external provider) at runtime.
  • Centralizing secret loading through one Settings object makes rotation and auditing tractable - grep the codebase for a single access pattern instead of scattered os.environ calls.

4. Apply Least Privilege to Every Credential and Process

Rationale: A leaked or compromised credential should grant the smallest possible blast radius. This applies to database users, cloud IAM roles, container runtime permissions, and CI pipeline tokens alike.

-- Application database user: scoped to exactly what the app needs
GRANT SELECT, INSERT, UPDATE ON accounts, transactions TO app_user;
REVOKE ALL ON schema_migrations FROM app_user;

Why this works well in production: If the application-tier credential leaks, the attacker cannot alter migration history, drop tables, or read unrelated schemas - containment is enforced at the data layer itself, not just application logic. The same logic applies to container users (see Docker Best Practices) and CI pipeline OIDC-scoped credentials (see CI/CD Patterns). This principle is not limited to credentials and database users - it applies equally to the tooling and execution environment itself (installers, build systems, runtime users), which is the specific focus of Autonomous, Least-Privilege, Portable Execution Environments.

5. Use Parameterized Queries and ORMs - Never String-Concatenate Untrusted Data Into Interpreters

Rationale: SQL injection, command injection, and template injection all share the same root cause: untrusted data concatenated into a string that is then interpreted as code.

# Wrong: vulnerable to SQL injection
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")

# Right: parameterized, driver escapes the value correctly for its context
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

Why this works well in production: The database driver treats email strictly as data, never as part of the query grammar, regardless of its content - this eliminates the entire injection class rather than attempting to filter dangerous characters, which is reliably incomplete.

6. Scan Dependencies Continuously, Not Just at Onboarding Time

Rationale: Transitive dependencies accumulate new CVEs continuously after a project is first built; a clean scan at project creation says nothing about risk six months later.

# CI pipeline step - fails the build on known-exploitable high-severity issues
- name: Software composition analysis
  run: pip-audit --strict --vulnerability-service osv

Why this works well in production: Wiring SCA (Software Composition Analysis) into every pipeline run (see CI/CD Patterns) turns dependency risk from a periodic manual audit into a continuously enforced gate, directly addressing the Software Supply Chain Failures category in the OWASP Top 10.

7. Design Secure Defaults Into Every Configuration Surface

Rationale: Defaults are what most deployments actually run, because most operators never change them. A framework or library that ships an insecure default (verbose debug output enabled, permissive CORS, disabled TLS verification) will have that insecurity reproduced across every downstream deployment that never overrides it.

class Settings(BaseSettings):
    debug: bool = False                 # never default to True
    cors_allowed_origins: list[str] = [] # empty, explicit allow-list required
    tls_verify: bool = True              # never silently disable certificate checks

Why this works well in production: An engineer under deadline pressure who never touches these settings still ends up with a secure configuration; insecurity requires an explicit, reviewable opt-out rather than being the silent default.

Common Pitfalls & Anti-Patterns

Pitfall: Relying on Client-Side Validation as the Only Check

Problem: Client-side validation (HTML5 required, JavaScript checks) can be trivially bypassed by calling the API directly with a tool like curl or a modified request.

Recommended approach: Treat client-side validation as UX only. Every constraint must be re-enforced server-side, as shown in Best Practice #1.

Pitfall: Blocklisting Dangerous Patterns Instead of Allowlisting Valid Ones

Problem: Attempting to filter out "bad" characters or patterns (<script>, ' OR '1'='1) is an endless, incomplete arms race - attackers routinely find encoding or obfuscation variants that bypass a blocklist.

Recommended approach: Define what valid input is (an allowlist: expected format, length, character set) and reject everything else, as in the pattern constraint in Best Practice #1.

Pitfall: Logging Sensitive Data

Problem: Passwords, tokens, full credit card numbers, or personally identifiable information (PII) written to application logs persist in log aggregation systems, often with weaker access controls than the primary database.

Recommended approach: Explicitly redact or omit sensitive fields before logging; use structured logging with an allowlist of loggable fields rather than logging entire request/response bodies by default.

logger.info("payment_processed", extra={"user_id": user.id, "amount_cents": amount, "card_last4": card.last4})
# Never: logger.info(f"Processing card {card_number}")

Pitfall: Rolling a Custom Cryptographic or Authentication Scheme

Problem: Custom password hashing, custom session token generation, or hand-rolled encryption routinely contain subtle flaws (timing side-channels, insufficient entropy, missing salt) that well-reviewed libraries have already solved.

Recommended approach: Use established libraries - passlib (bcrypt/argon2) for password hashing, the standard library's secrets module for token generation, and vetted TLS libraries for transport encryption. Never implement primitives from scratch.

Pitfall: Trusting Data From "Internal" Services Without Validation

Problem: In a microservices architecture (see Microservices Architecture), it is tempting to skip validation on service-to-service calls under the assumption that internal traffic is inherently trustworthy - but a single compromised or buggy internal service then becomes a vector into every downstream service.

Recommended approach: Apply the same input validation discipline at every service boundary, internal or external ("zero trust" networking principle); internal traffic should still be authenticated (mTLS or signed service tokens) and its payloads validated.

Testing Strategies

  • Static Application Security Testing (SAST): Run tools like Semgrep, Bandit (Python), or ESLint security plugins in CI to catch known-insecure patterns (string-formatted SQL, eval usage, hardcoded secrets) before merge.
  • Dependency/SCA scanning as a pipeline gate: pip-audit, npm audit, cargo audit, or trivy fs should fail the build on known-exploitable, fixable vulnerabilities (see Best Practice #6).
  • Negative testing for input validation: For every valid-input test case, write a corresponding invalid-input case (oversized payload, wrong type, boundary values, malicious patterns like ' OR 1=1--) and assert rejection, not just silent coercion.
  • Property-based testing for validation logic: Use Hypothesis (Python) or fast-check (JavaScript) to generate a wide range of malformed inputs automatically, surfacing edge cases manual test-writing misses.
  • Secrets-detection pre-commit and CI hooks: Tools like gitleaks or trufflehog scanning every commit and PR diff catch accidental secret commits before they reach a shared branch.
  • Cross-reference Integration Testing Strategies for how to test authentication/authorization flows end-to-end against a real (ephemeral) environment rather than mocked components only.

Performance, Security & Scalability Considerations

  • Validation cost is negligible compared to injection cost: Pydantic v2, Zod, and similar validators are implemented in compiled code (Rust/C) and add microseconds of overhead - never skip validation for performance reasons; the actual bottleneck is virtually always I/O.
  • Rate limiting as a secure-by-default control: Apply rate limiting at the API gateway or application middleware layer to blunt brute-force and enumeration attacks against authentication endpoints, independent of application-level validation.
  • Secrets rotation must be operationally cheap: If rotating a database password or API key requires a full redeploy and downtime, rotation will not happen on a healthy cadence after an incident. Design secret injection (environment variables sourced from a secrets manager, refreshed without redeploy where possible) so rotation is routine.
  • Horizontal scaling and shared secrets: In a scaled-out service, ensure all instances fetch secrets from the same centralized store rather than baking them into per-instance configuration, so rotation is atomic across the fleet.
  • Supply chain risk scales with dependency count: Every added dependency is additional attack surface; prefer a smaller, well-maintained dependency set and pin/lock versions (without disabling updates entirely) to keep the SCA scanning surface manageable.

Edge Cases & Advanced Usage

  • Multi-tenant data isolation: Beyond input validation, ensure every query is scoped to the authenticated tenant at the data-access layer (row-level security or repository-enforced tenant filtering), since a validation bug in one tenant's request must not be able to leak another tenant's data.
  • File upload handling: Validate file type by content (magic bytes), not just extension or client-supplied MIME type; store uploads outside the web root or in object storage with no execute permission, and re-encode/re-render images rather than serving user-uploaded files directly when possible.
  • Deserialization of untrusted data: Avoid language-native deserialization (Python pickle, Java native serialization) on any data crossing a trust boundary; prefer schema-validated formats (JSON via Pydantic/Zod, Protocol Buffers) that cannot execute arbitrary code during parsing.
  • Third-party webhook validation: Verify webhook payload signatures (HMAC with a shared secret) before processing, since webhook endpoints are public and otherwise indistinguishable from a forged request.
  • AI/LLM-assisted code generation: Treat AI-generated code with the same input-validation and injection scrutiny as any other contribution - generated code has been observed to reproduce insecure patterns (string-concatenated queries, missing validation) present in its training distribution.

When Not to Use / Alternatives

Secure coding practices are not a substitute for, but also not replaceable by, complementary controls.

DimensionSecure Coding (this doc)WAF / API Gateway ControlsNetwork Segmentation
Defends againstInjection, logic flaws, credential exposure at the code levelKnown attack signatures, volumetric abuseLateral movement after a breach
Requires code changesYesNo (external layer)No (infrastructure layer)
Catches zero-day application logic flawsYes (by design, not pattern-matching)No (signature/pattern based)No
Sufficient aloneNo - defense in depth requires all layersNoNo

A WAF or API gateway is not a substitute for input validation in code - it is a compensating control that reduces exposure while a code fix is deployed, and it cannot understand application-specific business logic flaws. Secure coding, network segmentation, and perimeter controls are complementary, not interchangeable.

Glossary

  • Trust boundary: The point at which data crosses from an untrusted source into a system's controlled logic.
  • SCA (Software Composition Analysis): Automated scanning of third-party dependencies for known vulnerabilities.
  • SAST (Static Application Security Testing): Automated analysis of source code for insecure patterns without executing it.
  • Least privilege: The principle that every credential, process, or component should hold the minimum access required for its function.
  • Secure default: A configuration state that is safe without requiring explicit developer action to make it so.
  • Allowlist / Denylist: An allowlist defines explicitly permitted values (recommended for validation); a denylist attempts to exclude known-bad values (unreliable in isolation).