Secure Coding Foundations

External data includes HTTP parameters, headers, cookies, uploaded files, message-queue payloads, database values, environment variables, command-line arguments, imported configuration, and third-party API responses.

3.1 Treat external data as untrusted

External data includes HTTP parameters, headers, cookies, uploaded files, message-queue payloads, database values, environment variables, command-line arguments, imported configuration, and third-party API responses. Data can become untrusted again when it crosses a new boundary or when its integrity is uncertain.

Do not confuse “came from our database” with “safe to use in an HTML page or shell command.”

3.2 Validate at boundaries

Use allowlists where practical: define the expected type, shape, length, range, and allowed values.

Tests appended for the run: Output (Node.js 24.18, TypeScript run with type stripping; tsc --strict also reports no type errors):

What it does: accepts unknown input, checks its structure, verifies that courseId is a bounded string using an explicit character policy, and returns a trusted application shape. A real API should translate validation errors into a consistent client error response without exposing internal details.

Important: validation is not authorization. A syntactically valid course ID may still refer to a course the user cannot access.

type EnrollmentRequest = {
  courseId: string;
};

function parseEnrollmentRequest(value: unknown): EnrollmentRequest {
  if (
    typeof value !== "object" ||
    value === null ||
    !("courseId" in value)
  ) {
    throw new Error("Invalid enrollment request");
  }

  const courseId = (value as { courseId: unknown }).courseId;

  if (
    typeof courseId !== "string" ||
    !/^[A-Za-z0-9_-]{1,64}$/.test(courseId)
  ) {
    throw new Error("Invalid course ID");
  }

  return { courseId };
}
const inputs: [string, unknown][] = [
  ["valid", { courseId: "sec-101" }], ["null", null], ["missing field", {}], ["number", { courseId: 42 }],
  ["65 characters", { courseId: "a".repeat(65) }], ["path-like", { courseId: "../admin" }], ["space", { courseId: "sec 101" }],
];
for (const [name, v] of inputs) {
  try { console.log(name.padEnd(14), "->", JSON.stringify(parseEnrollmentRequest(v))); }
  catch (e) { console.log(name.padEnd(14), "->", (e as Error).message); }
}
valid          -> {"courseId":"sec-101"}
null           -> Invalid enrollment request
missing field  -> Invalid enrollment request
number         -> Invalid course ID
65 characters  -> Invalid course ID
path-like      -> Invalid course ID
space          -> Invalid course ID

3.3 Validate business rules server-side

The browser may disable a button, but a caller can send requests directly. Enforce price, ownership, roles, enrollment limits, workflow state, and account status on the server.

Bad assumption: isAdmin: true from a client is proof of administrator privilege. It is only client-supplied data.

3.4 Use safe APIs

Parameterized SQL instead of query string concatenation HTML templates with contextual auto-escaping instead of raw HTML concatenation

Argument arrays instead of shell command strings Safe path APIs and fixed storage roots instead of string-based filesystem assumptions

Established cryptographic libraries instead of custom algorithms

Structured parsers instead of ad hoc parsing of complex formats

3.5 Error handling

Catch exceptions at meaningful boundaries.

Return stable, non-sensitive client errors.

Keep diagnostic detail in access-controlled logs.

Avoid empty catch blocks and “continue anyway” after a security failure.

Do not expose stack traces, SQL statements, local paths, tokens, or internal hostnames to untrusted clients.

Use timeouts and cancellation for external operations.

Output (CPython 3.13.1) with stub exception classes and an in-memory repository whose profile record also holds email and password_hash — fields the function deliberately leaves out:

The public response deliberately selects fields rather than serializing an entire database object. The internal log should not include secrets or unnecessary personal data.

def get_public_profile(user_id, repository):
    try:
        profile = repository.find_profile(user_id)
    except RepositoryUnavailable:
        # Log a correlation ID and operational details internally.
        raise ServiceUnavailable("Profile service is temporarily unavailable")

    if profile is None:
        raise NotFound("Profile not found")

    return {
        "id": profile.id,
        "display_name": profile.display_name,
    }
existing  {'id': 'u1', 'display_name': 'Ana'}
missing   NotFound: Profile not found
db down   ServiceUnavailable: Profile service is temporarily unavailable

3.6 Resource limits

Apply bounds to:

Request bodies and uploaded files Collection sizes and pagination JSON/XML nesting and parsing time Password-reset and login attempts Database query duration Concurrent jobs and queue depth Outbound calls and retries

A valid request can still be dangerous if it consumes unbounded resources.

Bounding a parser Output (CPython 3.13.1): The size check runs before parsing, so an oversized body never reaches the parser. Real frameworks expose body-size limits in configuration; set them there as well, and keep the depth check close to the code that walks the document.

import json

MAX_BYTES = 1_000
MAX_DEPTH = 5

def depth(value, level=1):
    if isinstance(value, dict):
        return max([level] + [depth(v, level + 1) for v in value.values()])
    if isinstance(value, list):
        return max([level] + [depth(v, level + 1) for v in value])
    return level

def parse_bounded(raw: bytes):
    if len(raw) > MAX_BYTES:
        raise ValueError("request body too large")
    value = json.loads(raw)
    if depth(value) > MAX_DEPTH:
        raise ValueError("document nested too deeply")
    return value

samples = {
    "normal": b'{"courseId": "sec-101"}',
    "too large": b'{"note": "' + b"x" * 2_000 + b'"}',
    "too deep": b"[[[[[[[[1]]]]]]]]",
}
for name, raw in samples.items():
    try:
        print(f"{name:<10} accepted: {parse_bounded(raw)}")
    except ValueError as error:
        print(f"{name:<10} rejected: {error}")
normal     accepted: {'courseId': 'sec-101'}
too large  rejected: request body too large
too deep   rejected: document nested too deeply

3.7 Secure code review checklist

Are all trust-boundary inputs validated?

Is authorization checked at the resource and operation level?

Are dangerous APIs used with safe arguments?

Can any exception reveal secrets?

Are memory, CPU, disk, and network consumption bounded?

Are secrets hard-coded, logged, or returned?

Is state changed atomically where needed?

Are security failures tested?

Are dependencies and configuration understood?

Practice

Review a simple registration endpoint. List at least ten possible failure cases and the corresponding test or control.

Quiz

? The browser hides the "Delete course" button for students. What does this protect against?
- Students deleting courses through the API :: A caller can send the request directly without the button.
+ Nothing on its own — the server must still check the permission :: UI hiding is a usability feature, not an access control.
- Cross-site scripting :: Unrelated.
- SQL injection :: Unrelated.

? `parseEnrollmentRequest` accepts `{"courseId": "sec-101"}`. What has been established?
+ The value has the expected shape and character policy. :: That is what validation decides.
- The user may enroll in `sec-101`. :: Validation is not authorization.
- The course exists. :: A well-formed ID can still name a missing course.
- The value is safe to place in any HTML or SQL context. :: Output contexts need their own encoding or parameterization.

? Which error response is appropriate for an untrusted client? (select all that apply)
+ A stable message such as "Profile service is temporarily unavailable" :: Clients get a useful, non-sensitive message.
- The full stack trace :: It reveals paths, libraries and internal structure.
- The failing SQL statement :: It reveals schema and query structure.
+ 
…