Working correctly is not the same as being secure
A piece of code can compile, pass its tests, run successfully in production, and behave correctly for every legitimate user while remaining insecure. Ordinary development proves what happens under conditions the team expects, but many security failures appear only when someone deliberately violates those expectations. The 2026 secure-coding guidance behind these examples focuses on this gap because routine engineering signals can stay green until hostile behavior supplies the missing test.
That gap often appears in ordinary engineering conveniences. A developer interpolates a value into a query, adds login middleware, returns a useful diagnostic, commits temporary credentials, relies on client validation, installs a package, or implements password handling locally. Each choice can work exactly as intended for legitimate use while leaving an adversarial condition unchecked. Security review has to examine that unchecked condition as part of normal engineering work.
The review starts where compilation, functional tests, normal production operation, and legitimate-user behavior cannot establish the property that matters. The cases differ in mechanism, but each needs an explicit constraint before release because normal feedback may never exercise the condition that breaks it. Input handling makes the problem especially visible.
Hostile input crosses boundaries that happy-path tests do not
Input handling exposes the mismatch because a runtime may give user-controlled data meaning the developer never intended. SQL injection, command injection, cross-site scripting (XSS), and path traversal attack different contexts, but each involves data entering a context that interprets it. A valid email, filename, HTML value, or shell argument exercises expected behavior. A hostile value tests whether the application preserves the boundary between user data and executable or address-like meaning.
SQL interpolation shows how that boundary can disappear. Consider a query assembled from a user-controlled email address:
js
const query = `SELECT * FROM users WHERE email = '$'`;
For an ordinary email address, the query does exactly what its author expects. If email contains ' OR '1'='1, the resulting WHERE condition matches every row. SQL injection is described as having remained on the OWASP Top 10 for as long as that list has existed, which helps explain why ordinary functional success is weak evidence here: expected inputs never challenge how the database interprets the value.
Parameterized queries create the missing boundary explicitly by supplying the SQL statement and user value separately:
js
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [email]);
With that separation, correctness no longer depends on every caller supplying an innocent string. The database API receives email as a parameter, so hostile characters inside the value do not become SQL syntax. Templating engines apply the same principle when they auto-escape values destined for HTML, preventing unescaped user input from becoming XSS payloads.
A shell introduces another interpreter, so the same boundary problem can produce command injection. When an application constructs a shell command from user-controlled content, special input can change what the shell executes. Expected test values can prove that the intended command works, but syntactically meaningful hostile values exercise a different property. Keeping untrusted values out of interpreted command text removes the application's dependence on cooperative input.
Filesystem paths extend the same reasoning to a resource address rather than executable code. An Express endpoint might take a filename from req.params.name, combine it with an upload directory, and read the resulting path:
"`js
const path = require('path');
const fs = require('fs');
app.get('/files/:name', (req, res) => );
"`
The endpoint succeeds for every expected filename, and path.join produces a normalized path. Yet a request for /files/..%2f..%2f..%2fetc%2fpasswd supplies encoded traversal segments that decode to ../. In this example, path.join('/app/uploads', '../../../etc/passwd') yields /etc/passwd, allowing an endpoint intended for uploaded files to return the system's user list.
The traversal example makes the required security property precise: whatever name the client supplies, the final path must remain beneath /app/uploads. The server can enforce containment after resolving the requested path. It resolves the upload root and requested location, confirms that the final path begins with uploads + path.sep, returns HTTP 400 with Invalid filename when the check fails, and reads the file only after the check succeeds:
"`js
app.get('/files/:name', (req, res) => );
"`
Together, these cases give teams two broad controls for interpreted input. Parameterized queries and auto-escaping templates preserve the separation between data and instructions, while resolved-path checks validate the interpreted result against an allowed boundary; file-access allowlists provide another option. The review question follows from the destination of user-controlled content: SQL, HTML, a shell command, and a filesystem path each need a control suited to the interpreter or resource they address.
A project in mind?
Schedule a 30-minute meeting with us.
Senior experts helping you move faster across product, engineering, cloud & AI.
Login and client checks do not prove a request is allowed
Even structurally safe input can produce an unauthorized result because a legitimate user may request a resource they have no right to access. Authentication establishes who the caller is. Authorization establishes whether that caller may perform a specific operation on a specific resource. Login middleware answers the authentication question, leaving authorization to be enforced where the application handles the resource.
An insecure direct object reference (IDOR), where a caller can request another user's object by changing its identifier, illustrates the gap. This Express route requires a login, then retrieves an invoice solely from the ID in the URL:
js
app.get('/api/invoices/:id', requireLogin, async (req, res) => );
Every legitimate request can work correctly, and the application can accurately know the caller's identity. Yet a logged-in user can change /api/invoices/:id to another invoice ID and receive that invoice because ownership was never part of the lookup. Sequential integer IDs make the weakness easier to exploit because a client can enumerate IDs with a simple for loop.
The missing authorization condition belongs in the server operation that fetches the object. Querying on both the requested invoice and the authenticated user's account constrains which object the request can produce:
js
const invoice = await Invoice.findOne();
That constraint is necessary for every request whose access depends on identity or ownership. A successful login establishes a principal, meaning the authenticated identity, while authorization connects that principal to an operation and resource. Without that connection, the application has evidence of identity without evidence of permission.
The server boundary also explains why client-side validation cannot enforce a security rule. Browser code can reject invalid values and improve the normal user experience, but a caller can bypass the browser and send an HTTP request directly to the API. Security-relevant validation belongs on the server, where the rule applies regardless of which client generated the request. Successful behavior through the official interface cannot establish that direct requests obey the same rule.
Security also fails when the application reveals too much
The same server boundary has an outbound side because a correctly handled request can still reveal internal information in its response. A production API, for example, returned a trace containing Error: connect ECONNREFUSED 10.0.3.42:5432, TCPConnectWrap.afterConnect, and /app/src/services/billing/stripe-sync.js:114. Those details identify internal IP address 10.0.3.42, indicate PostgreSQL, expose part of the project's directory structure, and reveal a Stripe integration.
Developers still need those diagnostics, so the application should retain them server-side and give the client a reference to the relevant event. It can generate a correlation or error ID, log that ID with the error and request path, and return an HTTP 500 response containing a generic message such as Something went wrong and the errorId. When a user contacts support, the team can use the ID in dedicated log-analysis tooling to find the detailed server event without publishing those details in the API response.
Smaller disclosures can also change an attacker's decisions. If a login or password-reset endpoint responds with "Invalid password" for an existing account and "No such user" for an unknown one, an attacker can determine which accounts exist. Returning the same generic result in either case removes that account-enumeration signal. The application can still distinguish the two states internally where necessary while exposing only the information the external interaction requires.
Some security-sensitive work should use established implementations
Restricting what crosses an application boundary still leaves another question: which security mechanisms should application teams implement themselves? Password handling shows why functioning custom code provides weak evidence. An authentication implementation may store and retrieve credentials successfully while using password storage that fails badly after a database breach. Plaintext storage is an obvious case, while fast hashes such as MD5 and SHA-256 are also unsuitable for passwords because their speed makes GPU brute-force attempts cheap enough to be a concern.
Password salts address a separate property. A salt is a unique random value generated for a password and mixed in before hashing, so two users who choose the same password receive different hashes. The salt can be stored alongside the hash and does not have to remain secret. Without unique salts, stolen hashes can be checked against rainbow tables, which are precomputed hashes for common passwords; unique salts force an attacker to work against accounts separately.
Password-specific algorithms combine these requirements in established implementations. With bcrypt, application code can hash and verify a password using calls such as:
js
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(attempt, hash);
bcrypt and Argon2 are deliberately slow and handle salts, which makes them better suited to password storage than fast general-purpose hashes. An appropriate password algorithm still leaves the application responsible for broader authentication behavior, so an identity provider or a battle-tested authentication library can move more security-sensitive work into an implementation designed and reviewed for that purpose.
The same delegation principle extends beyond authentication because established mechanisms can encode recurring constraints into routine development. Parameterized database APIs and auto-escaping templates reduce the bespoke security logic each application team must implement correctly and remember under deadline pressure. Pluralsight, a commercial technology-training provider that benefits when teams buy training, also provides a "Secure Coding learning path" and language-specific paths for Python, Java, C#, Go, and others. Training can help teams recognize where those established mechanisms belong, while the mechanisms enforce the relevant rule in running software.
Repositories and dependencies need controls before failure becomes visible
Established mechanisms matter outside application execution because a damaging event can also happen when code enters a repository. Temporary configuration files, hardcoded credentials added for a quick test, a missing .gitignore, or a committed .env-style file can all put a credential into repository history. Deleting the credential in a later commit leaves the earlier version in git, so anyone with suitable access to clone the repository can inspect its history.
Public repositories make the timing especially important. Automated bots are described as scanning public GitHub repositories and exploiting exposed credentials "in a matter of minutes." Once a secret has been pushed, teams should treat it as compromised and rotate it immediately. Rewriting git history does not restore confidence in a credential that has already been exposed.
Earlier controls can prevent some exposures before git records them. secretlint, for example, can run alongside a linter and inspect a commit for secrets:
bash
npm install –save-dev @secretlint/secretlint
The surrounding workflow can keep credentials in environment variables or a secrets manager and add secret scanning to Continuous Integration (CI), the automated pipeline that validates changes before delivery. Those controls move detection ahead of repository history and production use, where ordinary application tests have little reason to report that a credential became visible.
Dependencies create a related timing problem at the software-supply boundary. A typical Node application is described as pulling in "hundreds of dependencies," much of it code the application developer has never read. Careful package selection reduces risk, but a previously vetted dependency can later be compromised or acquire a disclosed vulnerability. The security state of the build can therefore change even when the application's own code and functional behavior remain the same.
That changing security state makes patch timing important because attackers are described as scanning for unpatched systems "within hours" of vulnerability disclosure. Functional tests can keep passing throughout that window, so successful application behavior does not tell the team whether a known vulnerable package remains in the build. Dependency security needs a signal tied to vulnerability information rather than application behavior.
CI can turn that external information into an engineering signal. Teams can run npm audit, or their ecosystem's equivalent, during CI and fail the build for high-severity warnings. Dependabot and Renovate can then deliver patch versions as small, reviewable pull requests, making dependency updates part of routine code review. The maintenance mechanism follows directly from the timing problem: the build needs to react when dependency risk changes.
Package selection adds a control before that maintenance cycle begins. A dependency with "three downloads a week" is given as a warning sign for closer scrutiny, while well-vetted packages are the preferred choice. Vetting addresses the decision to introduce a dependency, while continuous update automation addresses what happens after its security state changes. Together with pre-commit secret checks, these controls act before ordinary application failure can provide useful feedback.
Key highlights
- Build security into working software: Functional tests prove expected behavior, while security failures often emerge under hostile conditions. Engineering organizations need explicit security controls and adversarial testing before release.
- Protect every input boundary: Application teams should match controls to each destination, including parameterized SQL, HTML escaping, safe command execution, and filesystem containment checks. Server-side validation enforces these rules regardless of the client.
- Enforce authorization at the resource: Authentication establishes identity; each protected operation still needs a server-side permission check. Resource queries should incorporate ownership or account scope so changing an object ID cannot expose another user's data.
- Limit information exposed to clients: Application owners should keep stack traces, infrastructure details, and sensitive account signals in internal logs. Generic responses and correlation IDs preserve diagnostic value while reducing information disclosure.
- Use established security implementations: Engineering teams should rely on password-specific algorithms such as bcrypt or Argon2 and vetted authentication libraries or identity providers. Established implementations reduce bespoke security logic in high-risk areas.
- Detect repository and dependency risks early: Platform teams can add secret scanning and dependency audits to development and CI, rotate exposed credentials immediately, and automate package updates. These controls surface risks that functional tests cannot detect.
A project in mind?
Schedule a 30-minute meeting with us.
Senior experts helping you move faster across product, engineering, cloud & AI.


