← Blog
·10 blog.minutes

Secure Coding: A Developer's Guide to Writing Safe Code

Security is not the ops team's job. Every line of code you write either opens a door or bolts it shut. Here is what every developer needs to know about writing secure code in 2026.

There is a dangerous idea that has crept into our industry: that security is someone else's problem. That it belongs to the operations team, the security engineers, the dedicated penetration testers who show up once a quarter to poke at the application. This idea is wrong, and it is responsible for most of the breaches you read about on any given Tuesday.

The reality is that security is decided in the editor, not in the security review. Every decision you make — how you build a SQL query, how you handle user input, how you store a session token — either opens a door or bolts it shut. The attacker needs one open door. You need to close every single one.

This article covers the essential secure coding practices that every developer should know. It draws on the OWASP Top 10 for 2026, which has seen some significant shifts, and covers the vulnerabilities that appear in real codebases day after day: injection, cross-site scripting, cross-site request forgery, authentication failures, dependency risks, and secret exposure. Each section includes concrete, actionable patterns you can apply today.

The OWASP Top 10 in 2026: What Changed and Why

The OWASP Top 10 is the closest thing our industry has to a consensus list of the most critical web application security risks. The 2026 edition introduced several notable changes that reflect how the threat landscape has evolved. Understanding these shifts helps you focus your efforts on the risks that actually matter.

The biggest change in 2026 is the elevation of AI-generated code as a distinct risk category. For the first time, OWASP explicitly recognizes that code produced by large language models introduces unique security challenges. These models are trained on vast corpora of public code, which includes everything from well-maintained libraries to Stack Overflow snippets copy-pasted by developers who did not understand them. The AI reproduces the patterns it has seen, including the vulnerable ones. A 2025 study by Synk found that AI assistants suggested code containing known vulnerabilities roughly 30 percent of the time when given security-ambiguous prompts. The message is clear: AI-generated code needs the same level of scrutiny as code written by an unfamiliar contractor.

Another significant change is the consolidation of injection categories. Previous editions treated SQL injection, NoSQL injection, OS command injection, and LDAP injection as separate entries. The 2026 edition groups them under a single category called “Injection”, reflecting the reality that the underlying pattern — untrusted data concatenated into an interpreter — is the same regardless of the target. This is a useful reframing because it directs attention to the root cause rather than the specific variant.

Cryptographic failures have been promoted up the list, driven by the increasing prevalence of AI-powered cryptanalysis and the continuing disaster of quantum-adjacent migration planning. If you are not using modern authenticated encryption (AES-GCM, ChaCha20-Poly1305) and if you have not even started thinking about post-quantum migration for long-lived data, you are accumulating future debt that will come due with interest.

The 2026 list also merges security misconfiguration and software integrity failures into a broader category called “Configuration and Compromise”, recognizing that many integrity failures — such as using untrusted base images or unsigned dependencies — are fundamentally configuration decisions made early in the development lifecycle.

Security is not a product or a feature. It is a property of the engineering culture. If your team does not care about security during the design phase, no amount of scanning at the end will fix it.

Injection Attacks: The Same Old Story, Still the Biggest Problem

Injection attacks remain at the top of the OWASP list for a reason: they are simple to execute, devastating in impact, and surprisingly common in production code. The fundamental problem is that a program constructs a command or query by concatenating strings that include user-controlled input. The attacker crafts that input to break out of the intended context and inject their own instructions.

SQL injection is the classic example, and it still works. Developers who learned about SQL injection in university ten years ago still write vulnerable code under deadline pressure. The fix is well-understood: parameterized queries, prepared statements, or an ORM that handles escaping correctly.

// Vulnerable: string concatenation with user input
const query = `SELECT * FROM users WHERE email = '${req.query.email}'`;
db.execute(query);

// Fixed: parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [req.query.email]);

The vulnerable pattern should look obviously wrong to any experienced developer, yet it appears in code reviews every single week. The reasons are always the same: time pressure, a query that seemed too simple to bother with parameterization, or an ORM that was not available for the specific database driver. None of these reasons survives an incident post-mortem.

The same principle applies to NoSQL databases. MongoDB, for example, is vulnerable to injection when user input is passed directly into query objects. An attacker can pass { "$ne": "" } as a password value to bypass authentication entirely if the query is built from raw input.

OS command injection is another variant that refuses to die. Calling exec, spawn, or child_process with user-controlled input is catastrophic. If you need to run an external command, validate the input against a strict allowlist of permitted values. Never construct command strings from user input.

Cross-Site Scripting and the SPA Challenge

Cross-site scripting (XSS) is an injection attack where the target is the browser's DOM rather than a database. An attacker injects malicious JavaScript into a web page, and that script executes in the context of the victim's session, giving it access to cookies, localStorage, session data, and the ability to make requests on the user's behalf.

The rise of single-page applications built with React, Vue, and Svelte has changed the XSS landscape significantly. Modern frameworks automatically escape values in template expressions, which eliminates the most common XSS vectors — but developers can still bypass these protections. The most common bypass is dangerouslySetInnerHTML in React, v-html in Vue, or @html in Svelte. These APIs exist for legitimate use cases (rendering rich text from a CMS), but they open the door to XSS if the content is not sanitized first.

// Vulnerable: rendering unsanitized HTML from user input
function Comment({ body }) {
  return <div dangerouslySetInnerHTML={{ __html: body }} />;
}

// Fixed: sanitize before rendering
import DOMPurify from 'dompurify';

function Comment({ body }) {
  const clean = DOMPurify.sanitize(body);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Server-side rendering adds another dimension to XSS protection. If your application renders user-generated content on the server and sends HTML to the client, that content must be escaped on the server side. Many template engines default to escaped output (Handlebars, EJS with escaping, Pug), but developers can opt into unescaped output with helpers like triple-stash {{{ }}} or the pipe operator. Review every occurrence of unescaped output in your templates.

The third XSS vector that developers often overlook is indirect injection via data attributes and URLs. If you set a data attribute on a DOM element using user input, you might be vulnerable if the input contains characters that break the attribute context. Similarly, if you construct URLs from user input and inject them into anchor tags, an attacker can use javascript: URLs to execute arbitrary code.

Cross-Site Request Forgery and Authentication Hardening

Cross-site request forgery (CSRF) exploits the trust that a web application has in an authenticated user's browser. An attacker crafts a request to your application and tricks an authenticated user into executing it — by visiting a malicious page, clicking a link, or even loading an image. If your application relies solely on session cookies for authentication, the browser automatically includes those cookies with every request to your domain, making CSRF attacks possible.

The standard defense is a CSRF token: a unique, unguessable value embedded in every form and validated on the server. When a user submits a form, the server checks that the CSRF token matches the one stored in the session. An attacker's forged request cannot include this token because Same-Origin Policy prevents the attacker's page from reading your server's response.

// Vulnerable: no CSRF protection
app.post('/api/transfer', (req, res) => {
  const { toAccount, amount } = req.body;
  transferFunds(req.session.userId, toAccount, amount);
  res.json({ ok: true });
});

// Fixed: validate CSRF token
app.post('/api/transfer', (req, res) => {
  const token = req.headers['x-csrf-token'];
  if (!validateCsrfToken(req.session, token)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  const { toAccount, amount } = req.body;
  transferFunds(req.session.userId, toAccount, amount);
  res.json({ ok: true });
});

Modern frameworks include CSRF protection out of the box. Express.js middleware like csurf (deprecated but still used), Django's built-in CSRF middleware, and Rails' authenticity token mechanism all implement the same pattern. The mistake developers make is disabling CSRF protection for API endpoints that do not serve HTML forms, forgetting that those endpoints can still be targeted by cross-origin requests if proper CORS configuration is not in place.

Authentication hardening goes beyond CSRF. Every session should have an expiration. Every password reset token should be single-use and time-limited. Every login endpoint should implement rate limiting to prevent brute force attacks. Multi-factor authentication should be available and encouraged for all users, and required for administrative accounts.

Rate limiting is one of the most cost-effective security measures you can implement. It requires minimal code, has no ongoing maintenance burden, and prevents an entire class of attacks: credential stuffing, brute force, enumeration attacks, and many forms of API abuse. Implement rate limiting at the application level (per user), the network level (per IP), and the endpoint level (per sensitive operation). A reasonable starting point for login endpoints is five attempts per minute per user.

Dependency Management and Secret Handling

Modern applications are built on a foundation of open-source dependencies. A typical Node.js project has thousands of transitive dependencies. Each one is a potential attack vector. The 2024 event-stream incident, where a malicious actor gained maintainer access and injected cryptocurrency-stealing code into a popular npm package, is not an outlier — it is a warning about the systemic risk of the open-source supply chain.

  • Use a package manager with integrity verification. npm shrinkwrap and yarn.lock ensure that every install uses the exact same dependency tree. Pin your dependencies, do not use range operators.
  • Run dependency vulnerability scanning as part of your CI pipeline. Tools like npm audit, Snyk, Dependabot, and Trivy automatically detect known vulnerabilities in your dependency graph and block builds when critical vulnerabilities are present.
  • Remove unused dependencies. Every package you are not actively using is a liability with no benefit. Use tools like depcheck to identify dead weight in your package.json.
  • Audit the permissions requested by each dependency. A package that reads environment variables, accesses the filesystem, and makes network requests is a risk. Justify every permission.
  • Set up automated dependency updates with a review process. Dependabot or Renovate will open pull requests when new versions are available. Review the changelog, check for breaking changes, and merge promptly — especially for security patches.

Secret management is the other half of this equation. Hardcoded API keys, database passwords, and encryption secrets in source code are one of the most common findings in penetration tests. They are also entirely preventable.

// Vulnerable: hardcoded secrets in source code
const DB_PASSWORD = 'sup3r-s3cr3t-passw0rd!';
const API_KEY = 'sk-live-abc123def456';

// Fixed: use environment variables with validation
function getEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

const dbPassword = getEnv('DB_PASSWORD');
const apiKey = getEnv('API_KEY');

// Also: use .env files locally, never commit them
// Add .env to .gitignore from day one

If you have ever committed a secret to a git repository, it is compromised. Removing the secret from the latest commit is not enough — it is still in the git history. Anyone who has access to the repository can find it. The only safe course of action is to rotate the secret immediately and use a tool like git-filter-repo or BFG Repo-Cleaner to purge it from history if the repository is public or shared.

Use a dedicated secret manager in production. HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or Google Secret Manager provide secure storage, access auditing, automatic rotation, and fine-grained access control. Environment variables are acceptable for local development, but production secrets should be fetched from a secret store at application startup, not baked into environment configuration.

Tooling: SAST, DAST, and Security Headers

Secure coding is not just about what you write — it is also about what you check and how you deploy. The most disciplined development teams combine static analysis, dynamic analysis, and infrastructure hardening to create multiple layers of defense.

Static Application Security Testing (SAST) tools analyze source code without executing it. They look for patterns that indicate vulnerabilities: SQL injection, XSS, insecure deserialization, hardcoded secrets, and more. SAST tools integrate into the development workflow and provide feedback at the time the code is written, which is when it is cheapest to fix. Popular SAST tools include Semgrep, SonarQube, CodeQL, and ESLint with security plugins. The key to successful SAST adoption is to fix the noise level — tune the rules so that every alert is actionable, or developers will learn to ignore them.

Dynamic Application Security Testing (DAST) tools analyze a running application by sending crafted requests and observing the responses. They detect vulnerabilities that SAST tools cannot find, such as CSRF, authentication bypasses, and configuration issues. DAST tools include OWASP ZAP, Burp Suite, Acunetix, and StackHawk. DAST is most effective when integrated into the CI/CD pipeline and run against staging environments after deployment.

The third layer is infrastructure hardening through security headers. HTTP response headers tell the browser how to behave when rendering your application. Setting them correctly prevents an entire class of client-side attacks with no runtime cost.

  • Content-Security-Policy: Restricts which sources the browser can load resources from. Prevents XSS even if an attacker manages to inject a script tag.
  • Strict-Transport-Security: Forces the browser to use HTTPS for all connections to your domain. Prevents SSL stripping attacks.
  • X-Frame-Options: Prevents your application from being embedded in iframes on other domains. Mitigates clickjacking attacks.
  • X-Content-Type-Options: Prevents the browser from MIME-sniffing the response type. Reduces the risk of script injection via mislabeled content types.
  • Set-Cookie with Secure, HttpOnly, and SameSite flags: Ensures cookies are only sent over HTTPS, are inaccessible to JavaScript, and are not sent cross-origin. SameSite=Lax or Strict is the single most effective CSRF defense.

Security headers are easy to configure and immediately effective. Use a tool like securityheaders.com to scan your application and identify missing headers. Most application frameworks support setting these headers through middleware — Helmet for Express.js, the security middleware for Django, and Rack::Protection for Rails are all well-maintained and widely used.

The combination of SAST at development time, DAST at deployment time, and security headers at the infrastructure layer creates overlapping coverage. If one layer misses a vulnerability, another layer may catch it. This is defense in depth applied to the software development lifecycle.

Building a Security Culture

No tool, checklist, or framework can replace a development team that genuinely internalizes security as a design constraint. The difference between a team that writes secure code and a team that does not is not the quality of their static analysis tool — it is the set of habits and assumptions embedded in their daily workflow.

The most important habit is threat modeling before implementation. Before you write a single line of code for a new feature, ask: what is the attacker trying to do? What is the most valuable data this feature touches? What happens if an attacker controls the input? What happens if the database is compromised? Answering these questions early surfaces design-level vulnerabilities that no code review will catch because the vulnerability is in the architecture, not the implementation.

The second habit is peer review with security as a first-class concern. Code reviews that focus only on style, correctness, and performance miss security issues. Add a security checklist item to your pull request template: SQL injection? XSS? CSRF? Secrets in the diff? Authorization checks? Rate limiting? By making security part of the review process, you distribute the responsibility across the team and catch issues before they reach production.

The third habit is learning from incidents. When a security vulnerability is found — whether in your codebase or someone else's — treat it as a teaching opportunity. Hold a post-mortem that focuses on the systemic causes: why was the vulnerability introduced? Why was it not caught in review? What process change would prevent this class of vulnerability in the future? Blame-free post-mortems build a security culture. Blame-driven ones destroy it.

Secure coding is not a skill you learn once and are done with. The threat landscape evolves. The tools evolve. Your own understanding evolves. Every developer should invest time each quarter in learning about new attack vectors, new defense techniques, and new tools. Read the OWASP cheat sheet series. Follow security researchers on the platforms they publish on. Run a penetration test against your own application. The goal is not to achieve perfect security — that is not possible. The goal is to make your application a harder target than the next one, so the attacker moves on.

Because that is what it comes down to. Security is not about building an unbreakable system. It is about building a system that is not worth breaking compared to the alternatives. Every vulnerability you close, every secret you stop hardcoding, every rate limit you implement makes someone else a more attractive target. Do not be the low-hanging fruit.