September 30, 2026
What is secure coding, and which five practices stop most CWE Top 25 bugs?
Secure coding means picking the construct that makes a weakness impossible to write, then letting scanners confirm the habit held. The 2024โฆ

By Codeunderfire
5 min read
Secure coding means picking the construct that makes a weakness impossible to write, then letting scanners confirm the habit held. The 2024 CWE Top 25 places cross-site scripting, out-of-bounds write, SQL injection and path traversal in its first five ranks, and each maps to one coding decision that removes the class rather than the instance.
Table of Contents
- Why a scanner finding is really a coding decision
- Which secure coding practices remove whole weakness classes?
- Parameterised queries: why the injection cannot be expressed
- Output encoding: matching the fix to the sink
- Allowlist validation and the canonical path test
- Trust boundaries: what Log4Shell and Struts have in common
- Memory safety, and the checklist for the next pull request
Why a scanner finding is really a coding decision
Picture a pull request on an order-history endpoint in a small web service. The static scanner has left five findings on it, each pinned to a single line, and the reviewer has to decide whether the fixes are real or cosmetic.
The temptation is to treat findings as scanner noise. MITRE's 2024 CWE Top 25 argues otherwise: it is compiled from real CVE records, and it ranks cross-site scripting first, out-of-bounds write second, SQL injection third, cross-site request forgery fourth and path traversal fifth.
Which secure coding practices remove whole weakness classes?
The NIST Secure Software Development Framework describes itself as a core set of high-level practices for any development lifecycle, and it files the coding habits under producing well-secured software. The OWASP practice guide makes the same list concrete. Read together with the CWE ranking, five decisions cover most of the top of the list.
- Parameterised queries wherever text is assembled for SQL, LDAP, a shell or an OS command.
- Contextual output encoding wherever data is written into HTML, an attribute, a URL or a script.
- Allowlist validation with canonical paths wherever input reaches a file, a route or an identifier.
- Hard trust boundaries wherever a library interprets data: pinned versions, features off, no arbitrary deserialisation.
- Memory-safe languages or bounded operations wherever a buffer holds untrusted bytes.
The unifying idea is that each practice removes a class, not an instance. Escaping a quote fixes one input; a bound parameter removes the possibility of the query changing shape. That is the sense in which secure coding differs from bug fixing.
Parameterised queries: why the injection cannot be expressed
The first finding is the classic one. The endpoint concatenates a request parameter into a SQL string, so a value that closes the quote and opens a new statement rewrites the query. The pattern is not historical: in 2023 the MOVEit Transfer product shipped CVE-2023โ34362, a SQL injection that the Cl0p group used against more than 2,700 organisations, with over 93 million individuals affected by Emsisoft's tally.
The fix is a parameterised query. The SQL text travels with a placeholder, and the value travels separately in the bind step, so the driver never parses user data as SQL. The OWASP SQL Injection Prevention Cheat Sheet puts it precisely: parameterised queries force the developer to define all SQL code first and pass each parameter in later.
Output encoding: matching the fix to the sink
Findings two and three look different on the page but share a mistake: the fix has to match the place the data lands. The comment renderer assigns user text to innerHTML, so a script tag or an event-handler attribute runs in every visitor's session. The download handler joins a base directory with a filename from the request, so a run of dot-dot-slash segments walks out of the directory.
For the renderer the decision is contextual output encoding. The OWASP XSS Prevention Cheat Sheet points JavaScript authors at the textContent attribute and names it a safe sink, because the browser renders characters instead of parsing markup. The word contextual matters: HTML body, attribute, URL and script contexts each need a different encoder, and templating engines that autoescape by default handle most of that work.
Allowlist validation and the canonical path test
For the download handler the decision is allowlist validation with a canonical path. Resolve the joined path, collapse symlinks and dot segments, and reject anything that no longer starts inside the base directory. Then validate the filename against a known pattern or a set of identifiers rather than a list of bad characters, because encodings multiply faster than denylists.
Order is the subtle part. Apache HTTP Server 2.4.49 shipped CVE-2021โ41773 in 2021, a normalisation flaw that mapped requests outside the document root and was exploited within days, as the Apache httpd security page records. Normalising before the join and joining afterwards reopens exactly that hole, so the reviewer checks that the canonical test comes last.
Trust boundaries: what Log4Shell and Struts have in common
The fourth finding is a plain log call that records a username. In Log4j 2 before version 2.15.0, a logged string carrying a JNDI lookup made the library fetch and load a class from a remote server. That was CVE-2021โ44228, Log4Shell, which the NVD entry scores at CVSS 10.0, and it turned any log line fed by user input into remote code execution.
Deserialisation has the same anatomy. Apache Struts 2 shipped CVE-2017โ5638, an expression injection through the Content-Type header that the GAO report on the Equifax breach ties to records of about 147 million people. The decision is to treat every interpreting library as a trust boundary: pin it to a patched version, disable the interpreting feature for untrusted input, and refuse arbitrary object graphs in favour of a schema-bound format.
Memory safety, and the checklist for the next pull request
The last finding is a network buffer copied into a fixed-size array with an unbounded copy, so a long input overwrites adjacent memory and eventually the return address. Out-of-bounds write is second on the 2024 CWE Top 25, so this is the highest-ranked weakness on the pull request.
Google reports that memory-safety issues accounted for 76% of Android vulnerabilities in 2019 and 24% in 2024, and the Google Security Blog post behind that figure notes that the vast majority of vulnerabilities live in new or recently modified code. Where C or C++ remains, use bounded copies with the destination size and compile with hardening flags. For the next pull request, work the list in this order:
- Find every place a string is assembled for an interpreter and replace it with a bound parameter or an argument array.
- Trace every value written into HTML to its sink and make the sink a safe one or the encoder match the context.
- For every path, route or identifier from a request, add an allowlist check and a canonical-path test after the join.
- List the libraries that interpret data, pin them, disable the interpreting feature for untrusted input, and drop arbitrary deserialisation.
- For any new component touching untrusted bytes, pick a memory-safe language, and bound every copy in the code that stays.
Questions worth answering first
Does a web application firewall remove the need to fix the code?
No. A filter in front of the application blocks the payloads someone thought of, and the vulnerable construct is still sitting in the repository. Parameterised queries and contextual encoding remove the weakness itself, which turns the filter into a second layer rather than the only one.
Is a stored procedure the same thing as a parameterised query?
Only when the procedure binds its own inputs. A stored procedure that builds a SQL string inside the body and executes it dynamically carries the same injection, one layer deeper. The reviewer has to read the procedure body, not the call site.
Where does input validation fit if output encoding is the real fix?
Validation decides whether a value is acceptable at all, and encoding decides how it is written into a specific context. Use allowlist validation for paths, routes and identifiers, where the set of legal values is known in advance. Encoding belongs wherever any text can be legal, such as HTML bodies, attributes and URLs.
Do we have to rewrite existing services in a memory-safe language?
New code is where the benefit lands. Android's severe memory-safety bugs fell sharply once fresh components were written in Rust while the old ones stayed in place, so the practical rule is to write new parsers and network handlers in a memory-safe language and add bounds checks to whatever you keep.