Any value read from a form, URL, file, API response, message queue, or environment variable is untrusted until the program verifies it. That remains true when the value comes from an internal service: deployments change, integrations break, and attackers often look for one missed trust boundary. Good security work starts by making those boundaries visible in the code.
Security is built into ordinary development decisions: what the software accepts, exposes, permits, and does when something fails. The practices below help reduce common defects without requiring every developer to become a security researcher.
Validate input by allowing what is expected
Describe valid data instead of trying to list every dangerous string. A username might allow 3–32 letters, numbers, underscores, and hyphens. An order quantity may need to be an integer within a business-defined range. A country code may need to come from a small approved list.
Reject malformed values early, convert them to the appropriate type, and apply business rules before using them. Validate at every boundary where external or less-trusted data enters a component. Client-side checks help users correct mistakes, but server-side validation is what enforces the rule.
Validation is more than checking for empty fields
- Type: Parse numbers, dates, Boolean values, and identifiers instead of treating everything as free-form text.
- Length: Set sensible maximum sizes for request fields, uploaded content, headers, and batch records.
- Range: Confirm that numerical values stay within limits that make sense for the feature.
- Format: Check structured values such as email addresses, UUIDs, and dates according to the application’s needs.
- Business meaning: A valid account ID does not mean the current user may access that account.
A denylist can still help as an extra detection layer, but it should not be the main defense. Attack techniques and encodings change; a clear allowlist is usually easier to test and reason about.

Keep code separate from data
Injection happens when data is treated as instructions. SQL injection is the familiar example, but the same mistake can occur in shell commands, directory services, template engines, regular expressions, and document queries. The lasting fix is to use interfaces that keep executable syntax separate from supplied values.
For database access, use parameterized queries or an ORM feature that generates parameterized statements. Do not assemble queries by joining strings with user input. For operating-system tasks, use a language library when one is available rather than launching a shell. If a command is genuinely required, pass fixed program arguments through a safe process API, validate each variable argument against a strict allowlist, and run the process with minimal privileges.
Output also needs handling that matches its destination. Text rendered in HTML, inserted into an HTML attribute, placed in JavaScript, or included in a URL requires encoding for that exact context. HTML escaping is not automatically safe inside JavaScript. Modern template engines often escape HTML by default, but avoid bypassing that protection with “raw HTML” helpers unless the content has been carefully sanitized.
Use the right defense for the destination
| Situation | Preferred control |
|---|---|
| Database query with variable values | Prepared statements and bound parameters |
| User text rendered in a page | Context-aware output encoding |
| Limited rich-text content | Well-maintained HTML sanitization with an explicit allowed policy |
| File path selected from a request | Map an approved identifier to a server-controlled path |
| System operation | Native library or API instead of shell execution |
Authorize every sensitive action on the server
Authentication establishes who or what made a request. Authorization decides whether that identity may perform the requested action. Confusing the two creates serious access-control defects. For example, a signed-in user might view another customer’s invoice simply by changing an identifier in a request.
Check authorization for every protected operation, not just when showing a screen or menu. Hiding an admin button is a user-interface choice, not an access-control mechanism. The endpoint, service method, and data query should all preserve the intended rule.
A useful approach is to deny access by default. Define the roles, ownership rules, or attributes that permit an action, then reject requests that do not meet them. An application might allow a user to read a document only when it belongs to their organization and their role includes viewing permission. Make that comparison with trusted identity data from the session or token, not with a tenant ID supplied by the browser.
Handle credentials, tokens, and secrets deliberately
Passwords are not encryption keys and should never be stored in reversible form. Store them with a purpose-built adaptive password hashing algorithm, configured according to the library’s current recommendations. Each password needs a unique salt. During sign-in, the verification function compares the submitted password with the stored hash; the application should never recover the original password.
API keys, signing keys, database passwords, and cloud credentials need the same care. Do not place them in source code, examples, mobile applications, container images, or client-side scripts. Load secrets through a controlled runtime configuration mechanism, limit access to the smallest group that needs them, and rotate a secret when exposure is suspected.
Logs are often copied into monitoring and troubleshooting systems, which makes them a common exposure channel. Do not record passwords, session IDs, access tokens, full payment details, reset links, or sensitive personal data. When an identifier is needed for diagnosis, a masked or hashed form may still support correlation.
Fail safely without hiding operational evidence
Unexpected errors should not reveal stack traces, SQL fragments, server paths, internal IP addresses, or configuration values to end users. Return a simple, consistent error response and keep detailed diagnostic information in protected server-side logs. Include a request or correlation ID so support staff can connect a user-visible error to the relevant event.
Safe failure means choosing secure defaults. If an authorization service is unavailable, the application should not quietly grant access. If a file type cannot be identified, do not process it as trusted. If a cryptographic operation fails, stop the protected operation instead of falling back to something weaker.
Logs should still support investigation. Record the event type, time, request ID, authenticated principal when appropriate, target resource category, and result. Normalize untrusted text before writing it to logs, since control characters and misleading line breaks can make records difficult to interpret.
Use secure defaults in application configuration
Configuration mistakes can undo careful programming. Verbose errors, test credentials, permissive cross-origin rules, and debug endpoints should not reach production by accident. Keep environment-specific settings separate, review them like code, and make insecure options difficult to enable without notice.
Practical defaults to review
- Require encrypted transport for authenticated or sensitive traffic.
- Mark session cookies with Secure, HttpOnly, and an appropriate SameSite setting.
- Set timeouts, size limits, and rate limits for expensive endpoints.
- Disable unused features, sample accounts, administration consoles, and default credentials.
- Restrict cross-origin access to the specific trusted origins and methods the application requires.
- Use separate accounts and permissions for development, testing, and production systems.
Rate limiting needs one important distinction: it can reduce abuse and protect availability, but it is not an authorization control. An authenticated user must still have permission to perform an action, even when they are within the request limit.

Manage dependencies as part of the codebase
Third-party packages save time, but each dependency adds code and a maintenance obligation. Prefer established packages that address a defined need instead of adding a library for a small task the standard library can handle safely. Pin or constrain versions according to normal practices in your ecosystem, lock resolved dependencies where appropriate, and remove packages that are no longer used.
Check dependency advisories through authorized development workflows, then determine whether a reported issue affects the version and feature set you actually use. Updates need testing because a rushed upgrade can introduce regressions. Still, delaying known security fixes indefinitely creates avoidable risk. Keep an inventory of direct and transitive dependencies so the team can respond when an issue is disclosed.
Get tools and packages from official or trusted repositories, verify integrity where the ecosystem supports it, and avoid “cracked” development tools or unknown archives. For a lawful overview of safer ways to obtain software, see Kickass Torrents Alternatives for Developers: Safe, Legal Ways to Get the Tools You Need.
Design file handling and serialization defensively
File uploads are data, not proof that a file is harmless. A filename extension and a browser-supplied content type can both be misleading. Store uploads outside the web-accessible application directory where possible, generate filenames on the server, enforce size limits, and validate the file format with an appropriate parser. Never let a supplied filename determine a filesystem path without strict controls against traversal sequences and unexpected separators.
Deserialization is another boundary that needs special treatment. Avoid deserializing untrusted data into language objects when a simple typed format is enough. Parse JSON, XML, and other formats with safe library settings, define the expected schema, and do not enable features that instantiate arbitrary types from data received outside the trust boundary.
Make secure behavior testable
Security controls are more dependable when they are covered by tests. Add cases for invalid inputs, missing permissions, cross-account resource IDs, expired sessions, oversized uploads, and error paths. Confirm more than that a request fails: check that it returns the intended status, reveals no sensitive information, and causes no unwanted state change.
Code review works better when reviewers have focused prompts instead of a vague request to “check security.” A practical review pass can ask:
- Which values cross a trust boundary in this change?
- How are they validated, typed, and encoded for their destination?
- Which authorization rule protects each state-changing or data-reading operation?
- Could logs, errors, test fixtures, or configuration expose secrets?
- Did the change add a dependency, privilege, network call, or file operation that needs review?
For a final check, write one automated test with two ordinary accounts: create a resource as Account A, request it while authenticated as Account B, and verify that the response reveals neither the resource data nor details that confirm it exists. This small test catches a common object-level authorization failure before release.
