Join our Newsletter — 33% off our NHI Course
Home› Guides› Secure Coding Guide
Guide Application Security

Secure Coding Guide

← All guides
By Lalit Choda, NHI Mgmt Group Updated 17 October 2026 17 min read
On this page

Most exploitable software weaknesses are still written in by developers one line at a time, and the same handful of classes, injection, cross-site scripting, missing authorisation and memory corruption, top the list year after year. Scanners and penetration tests find some of them late; coding patterns that make them impossible remove them early and cheaply. This guide sets out the developer-level practices that matter most, from input handling, injection and authorisation to secrets, memory safety, dependencies and reviewing AI-written code. It is for developers, tech leads and application security engineers who set coding standards and review gates.

Key takeaways

  • Fix classes, not instances. Parameterised queries, auto-escaping templates, argument arrays instead of shell strings and memory-safe languages remove whole CWE classes; reviewing for individual bugs does not scale.
  • Validation is not the injection defence. Allowlist validation reduces attack surface, but OWASP is clear that SQL injection needs parameterisation and XSS needs context-aware output encoding.
  • Authorisation is now a top-five weakness. CWE-862 Missing Authorization rose five places to fourth in the 2025 CWE Top 25. Check object ownership on every request, server side.
  • Secrets never belong in code. Credentials committed to source, compiled files or packages get published; load them at runtime from a secrets manager and scan every commit.
  • AI-generated code is untrusted input to review. It reproduces insecure patterns and can suggest packages that do not exist; hold it to the same review gates as human code.

Where coding weaknesses concentrate

MITRE's 2025 CWE Top 25 (updated 15 December 2025) ranks weaknesses by frequency and severity in CVE records and counts how many map to CISA's Known Exploited Vulnerabilities (KEV) catalogue. CWE-79 cross-site scripting scored 60.38, more than double second place. CWE-78 OS command injection had the most KEV entries (20), followed by CWE-416 use after free (14) and CWE-787 out-of-bounds write (12). Three classic buffer overflow entries (CWE-120, CWE-121 and CWE-122) are new to the list.

RankCWEWeaknessPrimary coding control
1CWE-79Cross-site scriptingAuto-escaping templates, context-aware encoding, safe DOM sinks
2CWE-89SQL injectionPrepared statements, ORM bind parameters, allowlisted identifiers
3CWE-352Cross-site request forgeryFramework CSRF tokens, SameSite cookies
4CWE-862Missing authorisationDeny-by-default middleware plus per-object checks
5, 7, 8, 11, 14, 16CWE-787, 416, 125, 120, 121, 122Out-of-bounds write and read, use after free, buffer overflowsMemory-safe languages; hardening flags, sanitisers and fuzzing in C and C++
6CWE-22Path traversalResolve canonical path and check it stays under a base directory; map IDs to files
9, 23CWE-78, CWE-77OS command and command injectionAvoid shells; pass argument arrays to process APIs
15CWE-502Deserialisation of untrusted dataData-only formats (JSON) with schemas; no native object deserialisation of input
17, 24CWE-863, CWE-639Incorrect authorisation, user-controlled key (IDOR)Scope queries by tenant and owner; never trust IDs from the client
22CWE-918Server-side request forgeryAllowlisted destinations, block internal and metadata addresses after DNS resolution

Input validation and output encoding

The OWASP Input Validation Cheat Sheet says to "Validate both syntax (the expected type and format) and semantics (whether the value makes sense for the operation)", to validate on the server because "Client-side validation is bypassable", and to define what is accepted rather than "try to recognize every malicious string". Validation alone does not stop injection: a name field must accept an apostrophe (O'Brien), and stripping characters "still does not provide query parameterization or output encoding".

  • Validate early, encode late: check type, length and range at the trust boundary; encode or parameterise at the point of use.
  • Regular expressions: anchor the whole value, bound length first, and avoid nested quantifiers that cause ReDoS.
  • Canonicalise first: agree one character encoding, reject malformed input and normalise before validating.
  • Uploads (CWE-434): file name and Content-Type are attacker-controlled; "checking a filename is not checking the file's contents". Generate server-side names and store outside the web root.

The OWASP XSS Prevention Cheat Sheet ties each output context to an encoding rule:

Output contextRuleAvoid
HTML bodyHTML entity encode & < > " '; let the template engine auto-escapeDjango |safe, mark_safe on user data
HTML attributeAlways quote the value, then attribute-encodeUnquoted attributes, onclick handlers
JavaScriptOnly quoted data values, encoded for JavaScript; pass data via JSON in a data attributeUntrusted code or identifiers; eval
CSSProperty values only, CSS-encodedSelectors, url() built from input
URLPercent-encode parameter values, then attribute-encode for href; allow only https: schemesEncoding whole URLs; javascript: links
DOMtextContent, createTextNode, setAttribute with a fixed nameinnerHTML, React dangerouslySetInnerHTML, Angular bypassSecurityTrustAs*, Lit unsafeHTML

Where users must submit rich HTML, sanitise it (OWASP recommends DOMPurify) and do not modify it afterwards. A Content-Security-Policy is an additional layer that OWASP says "should not be your primary defense mechanism".

Injection: parameterised queries and safe APIs

OWASP's SQL Injection Prevention Cheat Sheet ranks prepared statements first, safe stored procedures second and allowlist validation third, and marks escaping as "STRONGLY DISCOURAGED" because "we CANNOT guarantee that this option will prevent all SQL injections in all situations".

# Python DB-API: the driver sends the value separately from the SQL text
cur.execute("SELECT id, email FROM users WHERE email = %s", (email,))

# Identifiers and sort order cannot be bound: map input to fixed values
SORT_COLUMNS = {"name": "name", "created": "created_at"}
column = SORT_COLUMNS.get(sort_param, "created_at")
direction = "DESC" if descending else "ASC"
  • ORMs are not automatically safe: raw query helpers such as Django raw() and extra(), string-built JPQL or HQL, and Sequelize literal queries need bind parameters too.
  • OS commands: call the program directly with an argument list, never a shell string. In Python, subprocess.run(["convert", src, dst], check=True) with the default shell=False; in Node.js, execFile or spawn rather than exec. Put -- before user-supplied file names so they cannot be read as options.
  • Database least privilege: one account per application, no DBA rights, read-only accounts for read paths, so a missed injection reaches less.
  • Other interpreters: LDAP, XPath, NoSQL operators (reject objects such as {"$ne": null} where a string is expected) and template engines each need their own parameter API or encoder.
  • Integer handling: CWE-190 overflow turns size calculations into buffer overflows. Use checked arithmetic (Rust checked_add, Java Math.addExact, __builtin_add_overflow) for lengths and offsets.

Authentication, sessions and authorisation in code

Use the framework's authentication and session libraries, configured to OWASP baselines.

  • Password storage: Argon2id with at least 19 MiB memory, two iterations and parallelism of one; scrypt (N=2^17, r=8, p=1) where Argon2id is unavailable; bcrypt (cost 10 or more, 72-byte limit) for legacy only; PBKDF2-HMAC-SHA256 with 600,000 iterations where FIPS 140 is required. See the Password Security Guide.
  • Session identifiers: at least 64 bits of entropy from a CSPRNG, regenerated "after any privilege level change" (login, password change, role change) to stop fixation, never accepted from URLs, and invalidated on the server at logout.
  • Timeouts and cookies: OWASP suggests idle timeouts of 2 to 5 minutes for high-value applications (15 to 30 for low-risk) and absolute timeouts of 4 to 8 hours, with __Host-SessionID=...; Secure; HttpOnly; SameSite=Strict; Path=/. See the Token and Session Security Guide.

Authorisation bugs (CWE-862, CWE-863, CWE-639) are logic errors that scanners rarely see. Three rules prevent most of them:

  1. Deny by default: require authentication and a permission on every route through central middleware or annotations, with an explicit, reviewed list of public endpoints.
  2. Check the object, not just the role: scope every lookup by owner or tenant, for example get_object_or_404(Invoice, pk=invoice_id, tenant=request.user.tenant) in Django, so a guessed ID returns nothing.
  3. Enforce at every entry point: server actions, GraphQL resolvers, background jobs and internal APIs are endpoints too. Next.js CVE-2025-29927, a middleware bypass, showed the risk of relying on one routing layer for authorisation; repeat the check next to the data access.

See the Authorisation Models Guide for choosing RBAC, ABAC or ReBAC.

Secrets and cryptography in code

API keys, database passwords and cloud keys in code are non-human identity credentials, and code is copied into forks, CI logs, container layers, compiled files and published packages. The PyPI secrets exposure found 3,938 secrets in published Python packages, and the PyPI admin GitHub token leak shows a token surviving in a compiled Python file inside a public Docker image.

  • Load at runtime: read secrets from a secrets manager or workload identity at start-up; prefer short-lived, federated credentials over static keys. See the Secrets Management Guide.
  • Scan before and after commit: pre-commit hooks, push protection on the repository host and history scans. A committed secret is compromised: rotate it, do not just delete the line. See the Leaked Credential Response Playbook.
  • Use vetted libraries, never your own algorithms: authenticated encryption (AES-GCM or ChaCha20-Poly1305) through a high-level library such as libsodium or Google Tink, TLS from the platform, and no ECB mode, MD5 or SHA-1 for security purposes. See the Cryptography Basics Guide.
  • Randomness and comparison: tokens from a CSPRNG (Python secrets.token_urlsafe(32), Java SecureRandom), never random or Math.random(); compare MACs and tokens in constant time (hmac.compare_digest).
  • Keys live in a KMS or HSM: code holds key identifiers only, with rotation designed in. See the Cryptographic Key Management Guide.

Error handling and logging

  • Fail closed: an exception in an authorisation or validation path must deny. A blanket catch that continues is a bypass.
  • Generic errors outward, detail inward: users get a message and a correlation ID; stack traces, SQL text and library versions go to server logs (CWE-209 and CWE-200). Turn off debug modes in production, such as Django DEBUG = False.
  • Log security events: OWASP lists input validation failures, authentication successes and failures, authorisation failures, session and JWT validation failures, and high-risk actions such as admin privilege use. Record when, where, who and what.
  • Never log secrets: exclude session IDs (hash them if correlation is needed), access tokens, passwords, keys, connection strings and card data. Redact in the logging library, not by convention.
  • Prevent log injection: sanitise carriage return, line feed and delimiter characters, or use structured JSON logging that encodes fields. See the Log Management and Retention Guide.

Memory safety and memory-safe languages

On 24 June 2025 NSA and CISA published the Cybersecurity Information Sheet "Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development". It notes that memory-safe languages "incorporate built-in mechanisms, such as bounds checking, memory management, and data race prevention" and that adoption "does not require existing code to be completely rewritten". Google reported that memory safety issues fell from 76% of Android vulnerabilities in 2019 to 24% in 2024, mainly by writing new code in memory-safe languages, because "vulnerabilities decay exponentially" as code ages.

  • New code first: write new components, especially parsers and network-facing services, in Rust, Go, Java, C#, Swift or another memory-safe language.
  • Rewrite selectively: target high-risk C and C++ such as codecs and protocol handlers, keep the safe and unsafe boundary narrow, and review every Rust unsafe block.
  • Harden what remains: follow the SEI CERT C and C++ standards (or MISRA C in safety-critical embedded work), compile with warnings as errors and hardening flags such as -D_FORTIFY_SOURCE=3, -fstack-protector-strong, -fPIE -pie and -Wl,-z,relro,-z,now, and test with AddressSanitizer, UndefinedBehaviorSanitizer and coverage-guided fuzzing.
  • Publish a roadmap: as CISA's memory safe roadmap guidance under its Secure by Design programme asks, inventory languages by component, set a date after which new code is memory-safe, and report progress.

Frameworks, dependencies and AI-generated code

  • JavaScript prototype pollution: merging user-controlled JSON into objects lets keys such as __proto__, constructor and prototype change every object. Validate input against a schema, use Map or Object.create(null) for dictionaries, and consider Node.js --disable-proto=delete.
  • Node.js and TypeScript: do not treat vm as a sandbox, set body size limits (CWE-770), and validate external data with a runtime schema; TypeScript types vanish at runtime.
  • Next.js: server actions are public endpoints; authorise inside each, and keep secrets out of client components.
  • Django: keep template auto-escaping, CSRF middleware and the ORM's parameterisation; review every |safe, mark_safe, raw() and extra(); run manage.py check --deploy before release.
  • Deserialisation: never use Python pickle, Java native serialisation or object-instantiating YAML loaders on untrusted data (use yaml.safe_load).

Coding standards must also cover what developers import: pin versions in lockfiles, prefer maintained packages with few transitive dependencies, and let software composition analysis flag vulnerable versions on every pull request. See the Open Source Security Guide.

AI coding assistants add a new failure: invented dependencies. A USENIX Security 2025 study by Spracklen and colleagues analysed 576,000 generated code samples and found hallucinated package names in at least 5.2% of packages from commercial models and 21.7% from open-source models, with 205,474 unique invented names that an attacker could register. Treat AI output as code from an unknown contributor:

  • Verify every new dependency exists, is the intended project and is approved, before it reaches a lockfile.
  • Apply the same gates: SAST, secret scanning and human review, with attention to missing authorisation, string-built queries and disabled TLS verification.
  • Constrain agents: coding agents that run commands or open pull requests need scoped tokens and approval steps. See the AI Coding Agents Security Guide.

Secure code review and rollout

Automated tools find patterns; reviewers find missing logic. Focus human review where tools are weakest.

TechniqueFinds wellMissesUse when
SAST (static analysis)Tainted data reaching injection sinks, dangerous APIs, hardcoded secretsMissing authorisation, business logicEvery pull request, blocking on new high findings
Secret scanningKeys and tokens in code and historyCustom secret formats without rulesPre-commit, push and scheduled history scans
SCA (composition analysis)Known-vulnerable and malicious dependencies, licencesFlaws in your own codeEvery build and on new advisories
DAST and fuzzingRuntime injection, crashes, memory errorsCode paths not reachedStaging builds; parsers and native code
Manual reviewAuthorisation, trust boundaries, crypto misuse, logic flawsVolume; reviewer fatigueAuth, payment, crypto, parsing and permission changes

Reviewer checklist for security-sensitive changes:

  • Is every new endpoint authenticated, and does it check the caller owns the object?
  • Does any input reach SQL, a shell, a template, a file path, a URL fetch or a deserialiser without parameterisation or an allowlist?
  • Are secrets, tokens or personal data written to code, logs, errors or client bundles?
  • Are new dependencies real, necessary, pinned and approved?

Rollout:

  1. Set the standard: a short, language-specific standard built on the OWASP Developer Guide checklists (the archived Secure Coding Practices Quick Reference moved there), the Cheat Sheet Series and SEI CERT rules, mapped to SSDF PW.5.
  2. Secure by default: provide paved-road libraries for database access, templating, SSRF-safe HTTP clients, authentication and secrets loading. Saltzer and Schroeder's psychological acceptability principle applies: controls that are hard to use get bypassed.
  3. Deploy tools in audit mode: run SAST, SCA and secret scanning without blocking for two to four weeks, tune false positives and record a baseline.
  4. Block on new findings only: fail pull requests for new high-severity issues and verified secrets; backlog the rest with due dates. Pilot with two or three teams first.
  5. Train against your own bugs: use the CWE classes your tools and pentests actually find.
  6. Roll back safely: keep gate rules in version-controlled policy so a noisy rule can be switched back to warn-only without disabling the scanner, and use time-limited, recorded exceptions instead of bypassing checks.

Small vs large organisations: a small team can rely on the repository host's built-in code scanning, secret scanning and dependency alerts plus a two-person review rule. Large organisations need security champions, central rule tuning and risk-tiered review, where only high-risk changes need an application security reviewer.

Measurement and framework mapping

  • Escape rate: share of vulnerabilities found in production or by pentest rather than before merge, by CWE class.
  • Recurrence: number of repeat findings in the same CWE class per repository after training or a paved-road fix.
  • Time to remediate: median days to fix high-severity code findings, against your policy target.
  • Secrets caught early: ratio of secrets blocked at pre-commit or push to secrets found in history or after publication.
  • Gate coverage: percentage of active repositories with SAST, SCA and secret scanning enforced on the default branch.
  • Memory safety trend: share of new code (or new components) written in memory-safe languages, quarter by quarter.

At runtime, send the authorisation and validation failures your code logs to the SIEM so endpoint probing is visible.

FrameworkReferenceRelevance
NIST SP 800-218 (SSDF v1.1)PW.4, PW.5, PW.6, PW.7, PW.8, RV.3Reuse secure components, secure coding, compiler and build hardening, code review, testing, root-cause analysis
NIST CSF 2.0PR.PS-06Secure software development practices integrated and monitored
NIST SP 800-53 Rev. 5SA-8, SA-11, SA-15, SI-10, SI-11Engineering principles, developer testing, development standards and tools, input validation, error handling
CIS Controls v8.116.1, 16.4, 16.9, 16.11, 16.12Secure development process, component inventory, developer training, vetted security modules, code-level checks
ISO/IEC 27001:2022 Annex AA.8.25, A.8.26, A.8.28, A.8.29Secure development life cycle, application security requirements, secure coding, security testing

Common failure patterns

  • Untrusted strings in shell commands, including CI: the Ultralytics PyPI compromise began with a GitHub Actions injection, where attacker-controlled input was interpolated into a workflow script. Pass untrusted values through environment variables, never inline expressions.
  • Secrets "removed" from code but not rotated: the value lives on in history, forks, caches and compiled files. The secrets in VS Code extensions research found 550 leaked secrets in published extensions, including tokens that could push updates to installed users.
  • Escape hatches as the default: dangerouslySetInnerHTML, |safe, verify=False and shell=True copied between files until they are normal. Flag each with a lint rule that requires a justification comment.
  • Scanner noise disabling the gate: an untuned SAST rollout that blocks every build gets switched off.

Practitioner checklist

  • Publish a language-specific secure coding standard mapped to the CWE Top 25 and SSDF PW.5.
  • Mandate parameterised queries and argument-array process calls; lint for string-built SQL and shell=True.
  • Enforce deny-by-default authorisation with per-object ownership checks on every endpoint and server action.
  • Store passwords with Argon2id or an OWASP-listed alternative and configure sessions to OWASP baselines.
  • Remove secrets from code, block them at commit and push, and rotate anything ever committed.
  • Set a date after which new components are written in memory-safe languages, and harden and fuzz the C and C++ that remains.
  • Verify every dependency an AI assistant proposes and apply the same review gates to AI-written code.
  • Track escape rate, recurrence and time to remediate by CWE class.

Standards and references

Related NHI Mgmt Group resources: Secrets Management Guide · Open Source Security Guide · AI Coding Agents Security Guide · Cryptography Basics Guide · Vulnerability Management Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Written and reviewed by Lalit Choda, NHI Mgmt Group. Last updated 17 October 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org