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
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.
Use the interface that keeps data out of syntax
Bind SQL values with placeholders, such as
con.execute(sql, (owner,))or a JDBCPreparedStatement. Start programs from an argument array with no shell, such assubprocess.run([exe, '--', path])orexecFile. Render text withtextContentor 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.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
newesttocreated_at DESCor 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.Turn off parser features the data does not need
Reject DOCTYPEs in untrusted XML (Java
disallow-doctype-decl, .NETDtdProcessing.Prohibit, Pythondefusedxml). Load YAML withsafe_load. Parse JSON into a checked schema instead of rebuilding native objects withpickle.loads,ObjectInputStreamorBinaryFormatter. Render only templates that live in your codebase and pass input as variables, neverrender_template_string(user_text).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).Flag the unsafe calls automatically
Write the team's banned list as rules, for example
shell=True,pickle.loads,yaml.load(,render_template_stringand 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.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 DEMOstarts 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 asO'Connorpassing, 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 checkCommon 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/pathpassed 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.
| Sink | Avoid with untrusted values | Use instead |
|---|---|---|
| SQL | SQL built with +, f-strings, % or .format() | Placeholders such as ?, $1 or :name with values passed separately; an allowlist map for column names |
| OS commands | os.system, shell=True, child_process.exec | An argument array with no shell: subprocess.run([...]), execFile, ProcessBuilder; -- before user values |
| HTML | innerHTML, dangerouslySetInnerHTML, v-html or |safe on input | textContent or escaped interpolation; a maintained sanitizer such as DOMPurify for rich text |
| Redirects and links | A URL taken from the request, checked with startsWith or a regex | A server-side id or local path; the parsed scheme, host and port against an exact allowlist |
| Outbound requests | Fetching any URL the user supplies and following its redirects | An exact host allowlist with redirects off; Constrain server-side fetches to their intended destinations for public URLs |
| XML | DTDs and external entities left on | disallow-doctype-decl (Java), DtdProcessing.Prohibit (.NET), defusedxml (Python) |
| Deserialization | pickle.loads, ObjectInputStream, BinaryFormatter, unserialize | JSON parsed into a checked schema |
| Templates and expressions | render_template_string(user_text), eval, SpEL with StandardEvaluationContext | Fixed template files with input passed as variables; SimpleEvaluationContext for SpEL |
Tools and guides
- Fix cards: tested fixes by stack
- Browser labs
- Workflow: fix a finding
- Tool: ReDoS checker
- Keep untrusted values out of executable syntax
Choose a safe interface for each interpreter and bound the work performed by parsers and converters.
- Prevent XSS at the output boundary
Choose text rendering, context-aware encoding, or deliberate HTML sanitization according to how the browser will interpret the value.
- Fix SQL injection in Python sqlite3
Bind query values separately from SQL syntax, then prove the fix preserves legitimate behavior and blocks an injection probe.
- Run external programs without command or argument injection
Run programs from an argument list with no shell, end options before user values, and keep untrusted arguments away from Windows batch files.
- Preserve framework security controls across the request lifecycle
Trace middleware, persistence and rendering so custom code does not bypass the protections supplied by the framework.
- Constrain server-side fetches to their intended destinations
Prevent untrusted URLs from turning a preview, import, or webhook feature into access to internal services.
- Redirect users only to destinations you control
Check every redirect target taken from a request, so attackers cannot borrow your domain for phishing, carry OAuth tokens off-site or steer a server-side fetcher through your redirects.
Standards
- NIST SSDF 1.1
- PW.5 Create Source Code by Adhering to Secure Coding Practices
Sources
- OWASP Injection Prevention Cheat Sheet
- OWASP Top 10:2025 A05 Injection
- OWASP ASVS 5.0.0: V1 Encoding and Sanitization. Version 5.0.0.
- OWASP Cross Site Scripting Prevention Cheat Sheet
- OWASP Unvalidated Redirects and Forwards Cheat Sheet
- OWASP Server Side Request Forgery Prevention Cheat Sheet
- CISA and FBI advisory AA23-158A: CL0P ransomware gang exploits CVE-2023-34362 MOVEit vulnerability
- Semgrep docs: writing rules
- CodeQL docs: CodeQL queries
- OWASP SAMM: Requirements-driven Testing
- NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1. February 2022.