Software Securityhome
Fix a findingSearch
Phase 3 · Development

Secure coding

Secure coding is writing code so that untrusted input stays data. Each value reaches its interpreter, such as a database, a shell, the browser or an XML parser, through an interface that keeps it apart from instructions, and the framework's built-in protections stay switched on.

Injection and XSS happen on one line, where a value meets an interpreter through the wrong API. Choosing the safe API as you write that line removes the bug class at that sink; scanners and reviewers can only find instances later, one at a time. MOVEit Transfer's CVE-2023-34362, which the CL0P ransomware gang began exploiting in May 2023, was an SQL injection: the kind bound parameters prevent.

Who
Every developer who writes code that handles input, with AppSec or a security champion keeping the team's list of safe and banned APIs
When
While writing the code, and again whenever a scanner, test, review or report finds a vulnerable pattern
How often
On every change; the rules that flag unsafe calls and the regression tests run on every pull request
You end up with
Code that uses the safe API at each sink, rules that flag the unsafe calls, and a regression test for each weakness class touched

Do this

  1. Find where untrusted values meet an interpreter

    List the sinks the change reaches: SQL and NoSQL queries, shell commands, HTML templates and DOM writes, redirects, outbound HTTP requests, XML and YAML parsers, deserializers and template engines. Treat stored data, third-party API responses and queue messages as untrusted too. The threat model (Threat modeling) shows which of these sit on a trust boundary.

  2. Use the interface that keeps data out of syntax

    Bind SQL values with placeholders, such as con.execute(sql, (owner,)) or a JDBC PreparedStatement. Start programs from an argument array with no shell, such as subprocess.run([exe, '--', path]) or execFile. Render text with textContent or the framework's escaped interpolation. OWASP prefers a safe API that avoids the interpreter or offers a parameterized interface, and treats escaping as a last resort.

  3. Map choices to fixed values where binding cannot help

    Column names, sort orders, file names and program options cannot be bound. Let the caller pick a keyword or id and map it in code, such as newest to created_at DESC or a report id to a stored file, and reject anything else. For redirects and server-side fetches, compare the parsed URL's scheme, host and port with an exact allowlist, never a prefix or regex check, and turn off redirect following in the HTTP client.

  4. Turn off parser features the data does not need

    Reject DOCTYPEs in untrusted XML (Java disallow-doctype-decl, .NET DtdProcessing.Prohibit, Python defusedxml). Load YAML with safe_load. Parse JSON into a checked schema instead of rebuilding native objects with pickle.loads, ObjectInputStream or BinaryFormatter. Render only templates that live in your codebase and pass input as variables, never render_template_string(user_text).

  5. Treat framework escape hatches as reviewed exceptions

    Frameworks escape output and bind queries by default, so most findings sit where code opts out: dangerouslySetInnerHTML, v-html, |safe, Html.Raw, raw SQL through the ORM, or a CSRF exemption. Allow each one only for a constant or the output of a maintained sanitizer such as DOMPurify, and send it to a reviewer (Security code review).

  6. Flag the unsafe calls automatically

    Write the team's banned list as rules, for example shell=True, pickle.loads, yaml.load(, render_template_string and SQL built with f-strings or +, as custom rules in a tool such as Semgrep or CodeQL. Run them on every pull request (CI security checks), so the next instance is caught as well as the one in review.

  7. Prove each fix with a test that fails on the old code

    For each weakness class the change touches, send a syntax-shaped value and assert the effect: ' OR 1=1 -- returns no rows, report; echo DEMO starts exactly one process, a name containing <img src=x onerror=...> renders as text with no image element. Run the test once against the vulnerable code to see it fail, and keep an ordinary value such as O'Connor passing, so a fix that rejects everything fails too.

Done when

Each weakness class the change touches has a test that fails on the vulnerable pattern.

Mark it in the gap check

Common mistakes

  • Escaping or blocklisting characters instead of using the safe API. Stripping quotes breaks valid names such as O'Connor, and the first context or encoding the filter did not expect brings the bug back.
  • Using one encoder for every context. HTML escaping does not make a javascript: link safe, and shell quoting is not SQL binding: each interpreter needs its own rule.
  • Removing the shell but letting input become an option. --output=/some/path passed in an argument array still changes what the program does; choose options in code and put -- before user values where the program supports it.
  • Catching a failure from the safe call and retrying the unsafe way, such as falling back to shell=True. That restores the vulnerability on exactly the inputs that triggered the fallback.
  • Trusting stored data. A display name saved safely becomes second-order SQL injection when a later export concatenates it into a new query.
  • Treating a fixed injection as fixed access control. A correctly bound query can still return another tenant's rows; keep the owner or tenant condition from the session in the query.

Which safe interface for which sink?

The safe choice depends on the interpreter that finally reads the value. OWASP ASVS 5.0 chapter V1, Encoding and Sanitization, holds the matching requirements, such as V1.2.4 for parameterized queries and V1.2.5 for OS commands.

Which safe interface for which sink?: table
SinkAvoid with untrusted valuesUse instead
SQLSQL built with +, f-strings, % or .format()Placeholders such as ?, $1 or :name with values passed separately; an allowlist map for column names
OS commandsos.system, shell=True, child_process.execAn argument array with no shell: subprocess.run([...]), execFile, ProcessBuilder; -- before user values
HTMLinnerHTML, dangerouslySetInnerHTML, v-html or |safe on inputtextContent or escaped interpolation; a maintained sanitizer such as DOMPurify for rich text
Redirects and linksA URL taken from the request, checked with startsWith or a regexA server-side id or local path; the parsed scheme, host and port against an exact allowlist
Outbound requestsFetching any URL the user supplies and following its redirectsAn exact host allowlist with redirects off; Constrain server-side fetches to their intended destinations for public URLs
XMLDTDs and external entities left ondisallow-doctype-decl (Java), DtdProcessing.Prohibit (.NET), defusedxml (Python)
Deserializationpickle.loads, ObjectInputStream, BinaryFormatter, unserializeJSON parsed into a checked schema
Templates and expressionsrender_template_string(user_text), eval, SpEL with StandardEvaluationContextFixed template files with input passed as variables; SimpleEvaluationContext for SpEL

Tools and guides

Standards

NIST SSDF 1.1
PW.5 Create Source Code by Adhering to Secure Coding Practices

Sources

  1. OWASP Injection Prevention Cheat Sheet
  2. OWASP Top 10:2025 A05 Injection
  3. OWASP ASVS 5.0.0: V1 Encoding and Sanitization. Version 5.0.0.
  4. OWASP Cross Site Scripting Prevention Cheat Sheet
  5. OWASP Unvalidated Redirects and Forwards Cheat Sheet
  6. OWASP Server Side Request Forgery Prevention Cheat Sheet
  7. CISA and FBI advisory AA23-158A: CL0P ransomware gang exploits CVE-2023-34362 MOVEit vulnerability
  8. Semgrep docs: writing rules
  9. CodeQL docs: CodeQL queries
  10. OWASP SAMM: Requirements-driven Testing
  11. NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1. February 2022.

Find your next answer

Type to search. Search runs in your browser.

Searches run in your browser, but the host can infer them from the index files they load. The address shows your search so you can bookmark or share it.