What Is Secure Coding? 12 Practices and a Developer Checklist
A practical secure coding guide covering input validation, authorization, secrets, dependencies, logging, testing, and a checklist for every release.
Primary source: authoritative reference
Secure coding is the practice of designing and implementing software so that it continues to enforce its security requirements when it receives unexpected input, runs with limited trust, or a component fails. It is not a final scan. It is a set of engineering decisions applied from design through deployment.
NIST's Secure Software Development Framework (SSDF) groups secure development work into preparing the organization, protecting software, producing well-secured software, and responding to vulnerabilities. The practices below turn that lifecycle guidance into checks a developer can use while writing and reviewing code.
Secure coding at a glance
- Define security requirements and trust boundaries.
- Validate input against an allowlist.
- Keep data separate from executable instructions.
- Encode output for its destination context.
- Use proven authentication and session components.
- Enforce authorization on every protected operation.
- Keep secrets out of code, builds, and logs.
- Minimize and continuously review dependencies.
- Fail safely without exposing internals.
- Log security events without logging sensitive data.
- Review and test the highest-risk paths.
- Ship fixes through a repeatable vulnerability-response process.
1. Define security requirements and trust boundaries
Start with what the software must protect, who can perform each action, and where data crosses between trust levels. Mark boundaries such as:
- Browser to API
- Public API to an internal service
- Application to database
- Build system to deployment platform
- First-party code to a third-party package or webhook
Turn those boundaries into testable requirements. “Only an account owner can download this invoice” is testable. “Use best-practice security” is not.
2. Validate input against an allowlist
Validate untrusted data on a trusted system before using it. Check expected type, length, range, format, and allowed values. Client-side validation can improve usability, but the server still needs to enforce the rule.
Prefer an allowlist:
const allowedSortFields = new Set(["createdAt", "name", "status"]);
if (!allowedSortFields.has(requestedSort)) {
throw new Error("Unsupported sort field");
}
Do not treat “sanitization” as a universal operation. Validation decides whether data is acceptable; output encoding and safe APIs prevent that accepted data from becoming executable in a particular destination.
3. Keep data separate from instructions
Use parameterized database queries instead of constructing commands with user-controlled strings:
# Unsafe: input becomes part of the SQL program
cursor.execute(f"SELECT * FROM users WHERE id = '{user_id}'")
# Safer: the driver sends code and data separately
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
Apply the same principle to operating-system commands, LDAP filters, template expressions, and other interpreters. Prefer a task-specific API that accepts structured arguments. If you are reviewing database code, the OWASP SQL Injection Prevention Cheat Sheet explains the limits of escaping and the role of prepared statements.
4. Encode output for its destination context
Output encoding is context-specific. Data inserted into HTML text, an HTML attribute, a URL, CSS, or JavaScript does not share one safe encoding rule.
Use the framework's default escaping and avoid bypasses such as raw-HTML rendering unless the application genuinely needs them. When users may author HTML, use a maintained HTML sanitizer with an explicit policy. See the OWASP Cross Site Scripting Prevention Cheat Sheet for context-specific guidance.
5. Use proven authentication and session components
Authentication answers who the subject is. Session management binds that authenticated state to later requests. Prefer mature framework or identity-provider components over a custom protocol.
For cookie-based web sessions:
- Use HTTPS for the entire session.
- Use
SecureandHttpOnly; choose aSameSitepolicy for the application's flow. - Regenerate the session identifier after authentication or a privilege change.
- Set idle and absolute timeouts appropriate to the risk.
- Invalidate the server-side session when the user logs out.
The OWASP Session Management Cheat Sheet provides the implementation details and tradeoffs.
Inspect a JWT without uploading it
Decode a token locally to inspect its header, payload, and time claims. Decoding does not verify the signature or prove that the token is trustworthy.
Open JWT Decoder →6. Enforce authorization on every protected operation
Authorization answers whether the authenticated subject can perform this action on this object. Enforce it server-side for every request, including background jobs and alternate API routes.
Check the relationship between the caller, action, and target object. Do not assume that an authenticated user may access a record merely because they supplied a valid record ID. Deny access by default and grant only the permissions required.
7. Keep secrets out of code, builds, and logs
Do not hardcode credentials or commit .env files. Environment variables can keep configuration separate from code, but they are not automatically a secure secret store. Control who can inspect the runtime, keep secrets out of logs and error reports, scope each credential narrowly, and revoke exposed credentials before investigating cleanup.
Use a managed secret store or workload identity when the platform supports it. For a deeper treatment, read Are Environment Variables Secure? and the environment-variable leak response guide.
8. Minimize and continuously review dependencies
Maintain an inventory of direct and transitive dependencies. Remove packages you no longer need, monitor the advisories that apply to your versions, and test updates before release.
Automated dependency alerts are inputs to a decision, not proof that a build is safe or unsafe. Prioritize findings using reachability, exposure, available mitigations, and the application's threat model. Pin or lock resolved versions where the ecosystem supports it, and protect the account and workflow that publishes your own packages.
9. Fail safely
Define the secure outcome for errors and partial failures. An authorization service timeout should not silently grant access. A failed validation step should not fall through to the protected operation.
Return useful but non-sensitive errors to users. Keep stack traces, filesystem paths, query text, and internal service details out of production responses. Record enough diagnostic context internally to investigate without recording credentials or tokens.
10. Log security events without logging sensitive data
Log events that help detect and reconstruct abuse, such as:
- Authentication successes and failures
- Authorization denials
- Administrative changes
- Validation failures at important trust boundaries
- Secret rotation and access-policy changes
Include a timestamp, event type, outcome, request or trace identifier, and the relevant subject or resource identifier. Avoid passwords, session identifiers, access tokens, raw secrets, and unnecessary personal data. Protect logs from tampering and restrict access.
11. Review and test the highest-risk paths
Automated tests should cover both allowed and denied behavior. Add tests for another user's object ID, a lower-privileged role, malformed input, boundary values, expired credentials, and dependency or network failures.
Use multiple techniques where they add evidence:
- Peer review for authorization and trust-boundary changes
- Static analysis for known code patterns
- Dependency and secret scanning
- Integration tests for access-control decisions
- Dynamic testing in a controlled environment
No single scanner establishes that an application is secure. Review results in the context of the code and its deployment.
12. Prepare to respond to vulnerabilities
A secure development process includes receiving vulnerability reports, assessing affected versions, creating and testing fixes, communicating mitigations, and learning from root causes. Keep release and rollback procedures repeatable so security fixes do not require improvisation.
NIST SSDF practice RV.3 recommends analyzing vulnerabilities to identify their root causes. The useful output is not only a patch; it is also a test, coding rule, or design change that prevents the same class of flaw from returning.
Secure coding checklist
Use this short list during review and before release:
- [ ] Security requirements and trust boundaries are documented.
- [ ] Untrusted input is validated server-side by type, length, range, and allowed value.
- [ ] Database queries and commands keep instructions separate from data.
- [ ] Output is encoded for the exact HTML, attribute, URL, CSS, or JavaScript context.
- [ ] Authentication and session handling use maintained, tested components.
- [ ] Every protected action checks authorization for the target object.
- [ ] Secrets are absent from source, build output, client bundles, and logs.
- [ ] Dependencies are inventoried, reviewed, and updated through a controlled process.
- [ ] Failure paths deny access and do not expose internals.
- [ ] Security events are logged without credentials, tokens, or unnecessary personal data.
- [ ] Tests cover denied behavior and abuse cases, not only the happy path.
- [ ] The team can receive, fix, release, and learn from vulnerability reports.
Sources
- NIST SP 800-218, Secure Software Development Framework (SSDF) 1.1
- OWASP Secure Coding Practices Checklist
- OWASP Authentication Cheat Sheet
- OWASP Authorization Cheat Sheet
- OWASP Logging Cheat Sheet
Secure coding is ordinary engineering performed with explicit trust boundaries, failure modes, and abuse cases in mind. Start with the checklist, adapt each control to the application's risk, and make the secure path the easiest path for developers to follow.