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.
| Rank | CWE | Weakness | Primary coding control |
|---|---|---|---|
| 1 | CWE-79 | Cross-site scripting | Auto-escaping templates, context-aware encoding, safe DOM sinks |
| 2 | CWE-89 | SQL injection | Prepared statements, ORM bind parameters, allowlisted identifiers |
| 3 | CWE-352 | Cross-site request forgery | Framework CSRF tokens, SameSite cookies |
| 4 | CWE-862 | Missing authorisation | Deny-by-default middleware plus per-object checks |
| 5, 7, 8, 11, 14, 16 | CWE-787, 416, 125, 120, 121, 122 | Out-of-bounds write and read, use after free, buffer overflows | Memory-safe languages; hardening flags, sanitisers and fuzzing in C and C++ |
| 6 | CWE-22 | Path traversal | Resolve canonical path and check it stays under a base directory; map IDs to files |
| 9, 23 | CWE-78, CWE-77 | OS command and command injection | Avoid shells; pass argument arrays to process APIs |
| 15 | CWE-502 | Deserialisation of untrusted data | Data-only formats (JSON) with schemas; no native object deserialisation of input |
| 17, 24 | CWE-863, CWE-639 | Incorrect authorisation, user-controlled key (IDOR) | Scope queries by tenant and owner; never trust IDs from the client |
| 22 | CWE-918 | Server-side request forgery | Allowlisted 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 context | Rule | Avoid |
|---|---|---|
| HTML body | HTML entity encode & < > " '; let the template engine auto-escape | Django |safe, mark_safe on user data |
| HTML attribute | Always quote the value, then attribute-encode | Unquoted attributes, onclick handlers |
| JavaScript | Only quoted data values, encoded for JavaScript; pass data via JSON in a data attribute | Untrusted code or identifiers; eval |
| CSS | Property values only, CSS-encoded | Selectors, url() built from input |
| URL | Percent-encode parameter values, then attribute-encode for href; allow only https: schemes | Encoding whole URLs; javascript: links |
| DOM | textContent, createTextNode, setAttribute with a fixed name | innerHTML, 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()andextra(), 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 defaultshell=False; in Node.js,execFileorspawnrather thanexec. 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, JavaMath.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:
- Deny by default: require authentication and a permission on every route through central middleware or annotations, with an explicit, reviewed list of public endpoints.
- 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. - 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), JavaSecureRandom), neverrandomorMath.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
unsafeblock. - 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 -pieand-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__,constructorandprototypechange every object. Validate input against a schema, useMaporObject.create(null)for dictionaries, and consider Node.js--disable-proto=delete. - Node.js and TypeScript: do not treat
vmas 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()andextra(); runmanage.py check --deploybefore release. - Deserialisation: never use Python
pickle, Java native serialisation or object-instantiating YAML loaders on untrusted data (useyaml.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.
| Technique | Finds well | Misses | Use when |
|---|---|---|---|
| SAST (static analysis) | Tainted data reaching injection sinks, dangerous APIs, hardcoded secrets | Missing authorisation, business logic | Every pull request, blocking on new high findings |
| Secret scanning | Keys and tokens in code and history | Custom secret formats without rules | Pre-commit, push and scheduled history scans |
| SCA (composition analysis) | Known-vulnerable and malicious dependencies, licences | Flaws in your own code | Every build and on new advisories |
| DAST and fuzzing | Runtime injection, crashes, memory errors | Code paths not reached | Staging builds; parsers and native code |
| Manual review | Authorisation, trust boundaries, crypto misuse, logic flaws | Volume; reviewer fatigue | Auth, 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:
- 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.
- 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.
- 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.
- 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.
- Train against your own bugs: use the CWE classes your tools and pentests actually find.
- 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.
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-218 (SSDF v1.1) | PW.4, PW.5, PW.6, PW.7, PW.8, RV.3 | Reuse secure components, secure coding, compiler and build hardening, code review, testing, root-cause analysis |
| NIST CSF 2.0 | PR.PS-06 | Secure software development practices integrated and monitored |
| NIST SP 800-53 Rev. 5 | SA-8, SA-11, SA-15, SI-10, SI-11 | Engineering principles, developer testing, development standards and tools, input validation, error handling |
| CIS Controls v8.1 | 16.1, 16.4, 16.9, 16.11, 16.12 | Secure development process, component inventory, developer training, vetted security modules, code-level checks |
| ISO/IEC 27001:2022 Annex A | A.8.25, A.8.26, A.8.28, A.8.29 | Secure 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=Falseandshell=Truecopied 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
- NIST: SP 800-218 Secure Software Development Framework (SSDF) Version 1.1
- MITRE: 2025 CWE Top 25 Most Dangerous Software Weaknesses
- OWASP: Secure Coding Practices Quick Reference Guide
- Carnegie Mellon SEI: SEI CERT Coding Standards
- NSA: NSA and CISA Release CSI Highlighting Importance of Memory Safe Languages in Software Security
- OWASP Cheat Sheet Series: Input Validation Cheat Sheet
- OWASP Cheat Sheet Series: SQL Injection Prevention Cheat Sheet
- OWASP Cheat Sheet Series: Cross Site Scripting Prevention Cheat Sheet
Related NHI Mgmt Group resources: Secrets Management Guide · Open Source Security Guide · AI Coding Agents Security Guide · Cryptography Basics Guide · Vulnerability Management Guide