Every vulnerability in production started as a line of code. SQL injection, cross-site scripting, buffer overflows, and insecure deserialization are all preventable at the coding stage. Fixing a vulnerability in development costs a fraction of what it costs in production, where it requires emergency patches, incident response, and potential breach notification.
Secure coding is not about adding security as an afterthought. It is about writing code that is resistant to abuse from the start.
The most fundamental secure coding principle: never trust user input. Every piece of data that comes from outside your application, whether from form fields, URL parameters, HTTP headers, file uploads, API requests, or database queries, must be validated before processing.
Validate on the server side. Client-side validation improves user experience but provides zero security. Attackers bypass it trivially by crafting requests directly.
Use allowlists over denylists. Define what valid input looks like (only alphanumeric characters, specific length range, expected format) rather than trying to block known-bad patterns. Denylists are always incomplete.
Validate type, length, format, and range. An age field should be an integer between 0 and 150. An email field should match email format. A file upload should be checked for both extension and MIME type (and ideally content analysis).
When displaying user-supplied data, encode it for the output context. Displaying a user's name in HTML? HTML-encode it so that renders as text, not as executable code. Inserting data into a JavaScript string? JavaScript-encode it. Building a SQL query? Use parameterized queries (never string concatenation).
Context-aware encoding is the primary defense against cross-site scripting (XSS). Libraries like OWASP's ESAPI and framework-provided encoding functions handle this correctly.
SQL injection remains one of the most impactful vulnerabilities. The fix is straightforward: never build SQL queries by concatenating strings. Instead, use parameterized queries (prepared statements) where user input is passed as parameters that the database treats as data, not as SQL code.
Every modern database library supports parameterized queries. There is no valid reason to use string concatenation for SQL in new code.
Do not implement your own authentication system unless you are a security expert. Use established libraries and frameworks (Spring Security, Django Auth, Passport.js, NextAuth). Implementing password hashing, session management, CSRF protection, and MFA correctly is complex and error-prone.
Store passwords using bcrypt, scrypt, or Argon2. Never use MD5, SHA-1, or SHA-256 for password storage (they are too fast and lack salting by default). Generate session tokens with cryptographically secure random number generators.
Error messages should help legitimate users without helping attackers. Never expose stack traces, database error messages, or internal system details to end users. Log detailed errors server-side for debugging; return generic error messages to the client.
Specifically: "Invalid username or password" is correct. "User not found" or "Incorrect password" tells an attacker whether the account exists.
Modern applications rely on hundreds of third-party libraries. Each one is a potential source of vulnerabilities. Use lockfiles (package-lock.json, Pipfile.lock, Gemfile.lock) to pin dependency versions. Scan dependencies regularly with tools like npm audit, pip-audit, or Snyk. Subscribe to security advisories for your critical dependencies. Remove unused dependencies.
Never hardcode secrets (API keys, database passwords, encryption keys) in source code. They end up in version control, CI/CD logs, and container images. Use environment variables at minimum, and a proper secrets management solution (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) for production.
Apply least privilege at the code level. Database connections should use accounts with only the permissions the application needs (read-only if it only reads). File system access should be scoped to specific directories. Network calls should be restricted to known endpoints. If a component is compromised, least privilege limits what the attacker can do.