Back to Blog
OpsecForge EditorialApplication SecuritySources reviewed 2026-10-24

Secure Coding Practices: What They Are and a Checklist by Development Stage

Secure coding practices are the habits that keep code enforcing its security rules when input is hostile or a component fails. Nine practices with code patterns, sources, and a design-to-deploy checklist.

Primary source: authoritative reference

Secure coding practices are the design and coding habits that keep software enforcing its security rules when input is hostile, a caller is untrusted, or a dependency fails. They are not a final scan. Each one targets a specific class of flaw, which is why this page does not try to teach all of them: it names the flaw, shows what it looks like in code, and sends you to the page on this site (and the primary source) that goes deep.

NIST's Secure Software Development Framework (SP 800-218) organizes this work into preparing the organization, protecting the software, producing well-secured software, and responding to vulnerabilities. The practices below cover the "producing well-secured software" part for web and API code. Use the stage checklist at the end if you want to jump straight to what to do when.

1. Check authorization on the object, not just the login

The most common API flaw is a handler that confirms the caller is logged in and then loads whatever ID they sent. The fix is to put the owner in the query or to check it before returning the object:

# Any logged-in user can read any invoice by guessing or enumerating IDs
invoice = db.get("SELECT * FROM invoices WHERE id = ?", (invoice_id,))

# The caller must own the row; a miss is indistinguishable from "not found"
invoice = db.get(
    "SELECT * FROM invoices WHERE id = ? AND owner_id = ?",
    (invoice_id, current_user.id),
)

Unpredictable IDs do not replace this check. Test it with two accounts: request A's object with B's token. Deep dive: BOLA prevention and testing. Sources: OWASP API1:2023, CWE-639, OWASP Authorization Cheat Sheet.

2. Bind only the fields a client may set

Passing a request body straight into an ORM model lets a client set fields you never put on the form, such as role or is_admin. Name the writable fields explicitly, per operation:

const { displayName, timezone } = parseProfileUpdate(req.body); // schema rejects extras
await db.user.update({ where: { id: currentUser.id }, data: { displayName, timezone } });

Deep dive: mass assignment and unsafe object binding. Sources: OWASP API3:2023, CWE-915, OWASP Mass Assignment Cheat Sheet.

3. Keep data out of the query text

Parameters cover values. They do not cover table names, column names, or sort direction, and ORM "raw" escape hatches reintroduce string building. Anything that selects an identifier must come from a fixed list in your code. Deep dive, with tested examples for SQLite, Django, SQLAlchemy and Sequelize: SQL injection in API code. To read a messy query before reviewing it, use the SQL Formatter. Sources: OWASP SQL Injection Prevention Cheat Sheet, CWE-89.

4. Validate input on the server and encode output for its destination

Validation answers "is this value acceptable?" with an allowlist of type, length, range, and format. Encoding answers "can this value become code where I am putting it?" and depends on the destination (HTML text, attribute, URL, script). Prefer APIs that do not parse markup at all:

el.textContent = comment.body;  // rendered as text
el.innerHTML = comment.body;    // parsed as HTML: stored XSS if body is attacker-controlled

If users must supply HTML, run it through a maintained sanitizer with an explicit policy rather than a regular expression. This site has no dedicated article on cross-site scripting, so rely on the sources: OWASP Input Validation Cheat Sheet, OWASP XSS Prevention Cheat Sheet, CWE-79.

5. Do not let user-supplied URLs reach internal networks

Any feature that fetches a URL for the user (link previews, image import, webhook targets) can be pointed at localhost or a cloud metadata address. A check on the hostname string is not enough, because the name can resolve differently when your HTTP client connects, and a redirect can lead somewhere the check never saw. The reliable shape is a host allowlist, a check of the IP address actually connected to, and redirects either disabled or validated hop by hop. Deep dive with code: SSRF prevention for APIs. Sources: OWASP SSRF Prevention Cheat Sheet, CWE-918.

6. Use current password rules and validate tokens fully

For passwords, NIST SP 800-63B-4 centers on length, screening against a blocklist of commonly used or compromised values, and not forcing arbitrary composition rules; see what it requires and how to calculate entropy and try the Password Generator. Store passwords with a dedicated password hashing function, not a plain hash (OWASP Password Storage Cheat Sheet).

For JWTs, the verifier must fix the accepted algorithm itself instead of trusting the token's header, and must check the claims it relies on (issuer, audience, expiry). Decoding a token is not verifying it. Deep dives: JWT vulnerabilities and safe validation and signing and issuing tokens; inspect a token locally with the JWT Decoder, which parses but does not verify signatures. Sources: NIST SP 800-63B-4, RFC 8725 (JWT best current practices), OWASP Authentication Cheat Sheet.

7. Verify webhook signatures over the raw body

A webhook endpoint is a public URL that anyone can POST to. Compute the HMAC over the exact bytes received (not re-serialized JSON) and compare in constant time:

const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const received = req.get("X-Hub-Signature-256") ?? "";
const ok = received.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

The length check comes first because timingSafeEqual throws on buffers of different lengths. Add a timestamp window and event-ID deduplication if the provider supports them. Deep dive with GitHub, Stripe and Slack examples: verifying webhook signatures; try it in the Webhook Signature Verifier. Sources: GitHub: validating webhook deliveries, CWE-347.

8. Keep secrets out of code, logs, and shared files

A credential in source, a build log, a client bundle, or a pasted .env is exposed to everyone who can read those. Load secrets at runtime, scope each one narrowly, and when one leaks, revoke it before you start cleaning up. Deep dives: are environment variables secure?, what to do after an environment-variable leak, API key leaks, and token exposure in CI/CD. Before sharing a config file for debugging, run it through the Safe-to-Share Sanitizer; its detection is heuristic, so read the output before you send it. Sources: OWASP Secrets Management Cheat Sheet, CWE-798.

9. Limit resource use and know every endpoint you expose

Endpoints without limits on request rate, payload size, query depth, or batch size invite denial of service and cost abuse, and a limit keyed only on IP address is easy to shift around. Separately, an endpoint you forgot (old versions, debug routes, paths that skip the gateway) has no controls at all. Deep dives: rate limit design, GraphQL batching and resource limits, API inventory and gateway boundaries, and HTTP request smuggling at the gateway. Sources: OWASP API4:2023, OWASP API9:2023.

Checklist by development stage

Design

  • Write down each trust boundary and who may do what to which object (practices 1, 9). Make each rule testable: "only the owner can download this invoice."
  • Decide where secrets live and how they rotate (practice 8: environment variables).
  • List every endpoint and its limits before launch (practice 9: API inventory).

Coding

  • Owner check in every object lookup (BOLA); explicit writable fields (mass assignment).
  • No string-built queries; identifiers from a fixed list (SQL injection).
  • URL-fetching features use an allowlist and check the connected IP (SSRF).
  • Webhooks verified over the raw body (signature guide); JWT algorithm and claims pinned (JWT validation).

Review

  • For every new route, ask who else's data it returns if the ID changes, and which request fields reach the database.
  • Add tests for the denied path: another user's ID, a lower role, an extra JSON field, an unsigned webhook.
  • Search the diff for secrets and for pasted config; sanitize shared files with the Safe-to-Share Sanitizer.

Deploy and operate

  • CI credentials are short-lived and scoped (CI/CD token exposure); leak response is rehearsed (leak response).
  • Rate and size limits are active at the gateway and the application (rate limits).
  • Logs record authentication and authorization failures without tokens or passwords (OWASP Logging Cheat Sheet).
  • You can receive a vulnerability report, ship a fix, and add a test or rule so the same flaw does not return (NIST SSDF group RV).

Sources

Share this: