Common Web Security Mistakes
A practical guide to common web security mistakes in websites and APIs, with examples, prevention techniques, and a production security checklist.
Web security problems are often caused by ordinary development decisions rather than sophisticated attacks. A missing authorization check, an API key accidentally committed to Git, an overly permissive CORS policy, or an outdated security header can create a vulnerability even when the rest of the application is well designed.
Many security mistakes are also easy to overlook because an application can work perfectly from a functional perspective while still exposing users or data to attackers. Security therefore needs to be considered separately from whether a feature behaves correctly.
This guide covers common mistakes found in modern websites, web applications, and APIs. It focuses on practical problems developers can identify during development, code review, deployment, and ongoing maintenance.
Common Web Security Mistakes at a Glance
| Mistake | Typical consequence |
|---|---|
| Missing authorization checks | Users can access resources they should not control. |
| Trusting client-side validation | Attackers bypass browser-side restrictions. |
| Unsafe HTML rendering | Cross-site scripting vulnerabilities. |
| Weak password storage | Credential databases become easier to crack after a breach. |
| Poor session management | Account takeover or session theft. |
| Exposed API keys | Unauthorized access or unexpected costs. |
| Hardcoded secrets | Credentials leak through source code or repositories. |
| Overly permissive CORS | Unexpected cross-origin access to application resources. |
| Missing security headers | Reduced browser-level protection. |
| Ignoring CSRF | Unauthorized state-changing requests. |
| Unsafe file uploads | Malicious files or server-side attacks. |
| SQL injection | Unauthorized database access or modification. |
| Verbose error messages | Internal implementation details are exposed. |
| Outdated dependencies | Known vulnerabilities remain exploitable. |
| Missing rate limits | Brute force, abuse, or resource exhaustion. |
1. Trusting Client-Side Validation
Client-side validation improves user experience, but it is not a security boundary. An attacker can disable JavaScript, modify requests, call an API directly, or use a completely different client.
if (amount > 0 && amount <= 1000) {
submitPayment(amount);
}The browser may prevent a normal user from entering an invalid amount, but the server must validate the amount again. Every security-sensitive constraint must be enforced on the server or another trusted component.
2. Missing Server-Side Authorization
Authentication answers the question of who a user is. Authorization determines what that user is allowed to access or change. A user being logged in does not mean they can access every resource identified by an ID in a request.
const order = await db.orders.findUnique({
where: { id: request.params.id },
});
return Response.json(order);The example retrieves an order by ID but does not verify that the authenticated user owns it or has permission to access it. An attacker who changes the ID could potentially access another user's order.
const order = await db.orders.findFirst({
where: {
id: request.params.id,
userId: currentUser.id,
},
});
if (!order) {
return new Response("Not found", { status: 404 });
}Authorization should be checked against the actual resource and operation. This is especially important for APIs where object identifiers are easy to modify.
3. Assuming Hidden IDs Provide Security
Using UUIDs instead of sequential numeric IDs can make enumeration harder, but an unpredictable identifier is not an authorization mechanism. If a user knows or obtains another user's UUID, the server still needs to verify access.
Security should depend on explicit authorization rules rather than on an identifier being difficult to guess.
4. Storing Passwords as Plain Text
Passwords should never be stored as plain text. If a database is compromised, plain-text passwords immediately expose users and may also expose accounts on other services where users reused the same password.
Password storage should use a password-specific hashing algorithm such as Argon2id or bcrypt with appropriate parameters. General-purpose hashes such as SHA-256 are designed to be fast, which is undesirable for password storage because attackers can test large numbers of guesses quickly.
const hash = await argon2.hash(password);
const valid = await argon2.verify(hash, passwordAttempt);5. Using Weak Authentication
Authentication systems can become vulnerable through weak password requirements, predictable reset tokens, missing multi-factor authentication for sensitive operations, poor session management, or inadequate brute-force protection.
Security-sensitive applications should consider stronger authentication mechanisms such as MFA or passkeys where appropriate. Password reset and account recovery flows deserve the same level of attention as the normal login flow.
6. Poor Session Management
A secure login can still be undermined by insecure sessions. Session identifiers should be unpredictable, protected during transport, and invalidated when appropriate.
Set-Cookie: session=RANDOM_VALUE; Secure; HttpOnly; SameSite=LaxSecure prevents the cookie from being sent over ordinary HTTP, HttpOnly prevents normal JavaScript access, and SameSite controls cross-site cookie behavior. Exact SameSite settings should match the application's authentication and cross-origin requirements.
7. Exposing API Keys in Frontend Code
Anything shipped to a browser should be considered visible to the user. Putting a secret API key into frontend JavaScript, even behind an environment variable, does not make the key secret if the value is included in the client bundle.
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: "Bearer SECRET_API_KEY",
},
});A public frontend should normally call your own server-side endpoint when a third-party API requires a secret credential. The server can then keep the credential outside the browser.
8. Hardcoding Secrets in Source Code
Database passwords, API keys, signing secrets, private keys, and other credentials should not be hardcoded into application source code.
const databasePassword = "super-secret-password";Source code can end up in Git repositories, build artifacts, logs, backups, pull requests, and developer machines. Removing a secret from the latest commit does not necessarily remove it from repository history.
Use environment variables or a dedicated secret-management system for sensitive configuration. If a secret has already been exposed, rotating the credential is more important than simply deleting the visible copy.
9. Committing Secrets to Git
A common variation of hardcoded secrets is accidentally committing a .env file or configuration containing production credentials.
DATABASE_URL=...
API_SECRET=...
PRIVATE_KEY=...A .gitignore entry helps prevent future accidental commits, but it does not remove credentials that were already committed. Exposed credentials should be considered compromised and rotated.
10. Rendering Untrusted HTML
Cross-site scripting, or XSS, can occur when untrusted data is interpreted as executable HTML or JavaScript. React and similar frameworks provide useful escaping by default, but developers can bypass those protections with APIs intended for raw HTML rendering.
return <div dangerouslySetInnerHTML={{ __html: userContent }} />;If raw HTML is genuinely required, it should be sanitized using an appropriate HTML sanitizer and the application's security model should be reviewed carefully.
11. Building SQL Queries With String Concatenation
Constructing SQL queries by concatenating untrusted input can create SQL injection vulnerabilities.
const query =
"SELECT * FROM users WHERE email = '" + email + "'";Use parameterized queries, prepared statements, or a database library that safely binds values instead of inserting untrusted strings directly into SQL.
const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[email],
);12. Using Unsafe Dynamic Commands
The same principle applies outside SQL. Passing untrusted input into operating-system commands, shell execution, template engines, or other interpreters can result in injection vulnerabilities.
Avoid passing raw user input to interpreters whenever possible. Prefer structured APIs that accept individual arguments and validate them against strict allowlists.
13. Overly Permissive CORS
Cross-Origin Resource Sharing controls which browser origins can make certain cross-origin requests. A common mistake is allowing every origin without considering whether authenticated responses can be exposed.
Access-Control-Allow-Origin: *A wildcard is not automatically insecure. Its appropriateness depends on whether the resource is intended to be publicly readable and whether credentials are involved. Problems arise when a sensitive API is exposed through an overly broad cross-origin policy.
CORS is also not an authorization system. A server must still enforce authentication and authorization regardless of whether a browser is permitted to read the response cross-origin.
14. Ignoring CSRF Protection
Cross-Site Request Forgery can occur when a user's browser automatically includes authentication credentials with a request initiated from an attacker-controlled site.
Modern applications can use SameSite cookies, CSRF tokens, origin checks, and other mechanisms depending on the authentication architecture. Applications using cookie-based authentication should explicitly consider CSRF rather than assuming that CORS solves the problem.
15. Missing Security Headers
Browsers provide security mechanisms that can be strengthened through HTTP response headers. Failing to configure appropriate headers can leave an application without useful browser-level defenses.
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-originThe exact policy should depend on the application. For example, an overly restrictive Content-Security-Policy can break legitimate scripts, styles, analytics, or third-party integrations.
16. Disabling Browser Security Controls to Make Features Work
Developers sometimes weaken a security control because an integration does not work with the default configuration. Examples include disabling CSP, allowing every origin through CORS, removing cookie restrictions, or permitting insecure content.
The better approach is to identify the exact requirement and create the narrowest exception that supports it. A security control should not be removed globally simply because one feature needs additional configuration.
17. Returning Sensitive Data in API Responses
An API can have correct authentication and authorization while still returning more data than the client needs. Sensitive fields such as password hashes, internal identifiers, reset tokens, private notes, or administrative metadata should not be included in ordinary responses.
{
"id": "123",
"email": "[email protected]",
"passwordHash": "...",
"internalRole": "admin"
}Define response schemas deliberately and return only the fields required by the client. Database models should not automatically become public API models.
18. Verbose Production Error Messages
Detailed errors are useful during development but can reveal database structures, file paths, internal service names, stack traces, dependency versions, or implementation details in production.
{
"error": "PostgreSQL connection failed at /app/src/db/client.ts:42"
}Production responses should provide enough information for the client to understand that an operation failed without exposing unnecessary internal details. Detailed diagnostics should remain in protected server-side logs.
19. Missing Rate Limiting
Without rate limiting, public endpoints can be abused for brute-force attacks, credential stuffing, automated scraping, expensive AI requests, password-reset abuse, or resource exhaustion.
Rate limits should be designed around the operation. Login attempts, password resets, registration, search, expensive API operations, and anonymous requests may need different limits.
Rate limiting should not be treated as a replacement for authentication or authorization. It is an additional abuse-control mechanism.
20. Accepting Unlimited File Uploads
File-upload endpoints introduce several security risks. An attacker may attempt to upload extremely large files, executable content, malicious documents, unexpected file types, or files designed to exploit image and document-processing libraries.
- Limit file size.
- Restrict accepted file types.
- Validate the actual file content rather than trusting only the extension.
- Generate safe server-side filenames.
- Store uploads outside executable application directories.
- Use malware scanning where appropriate.
- Restrict image and document processing resources.
- Avoid exposing uploaded files with unnecessary execution permissions.
21. Trusting File Extensions
Checking only a filename such as image.jpg is not sufficient to establish that the uploaded content is actually a JPEG image. File extensions are controlled by the client.
Servers should validate file content using appropriate parsers or file-signature checks and should apply application-specific restrictions to the resulting data.
22. Forgetting Dependency Security
Third-party packages become part of an application's attack surface. A vulnerability in a dependency can affect an otherwise carefully written application.
- Keep dependencies reasonably up to date.
- Monitor security advisories.
- Remove packages that are no longer needed.
- Review transitive dependencies for important applications.
- Lock dependency versions appropriately.
- Test upgrades before deploying them to production.
Updating every dependency immediately is not always practical, but ignoring security advisories indefinitely creates unnecessary risk. Security updates should have a defined maintenance process.
23. Using HTTP for Sensitive Traffic
Sending credentials, session identifiers, personal information, or API data over plaintext HTTP exposes that traffic to network interception and modification.
Use HTTPS throughout the application and configure appropriate redirects and HSTS where suitable. The security of the transport should be verified from the actual production endpoint rather than assumed from local configuration.
24. Treating JWTs as Automatically Secure
Using JWT does not automatically make an authentication system secure. Developers still need to validate signatures, algorithms, expiration, issuer, audience, and other claims according to the application's requirements.
const payload = verifyJwt(token, {
issuer: "https://auth.example.com",
audience: "api",
});JWT payloads are generally encoded rather than encrypted. Sensitive information should not be placed in a JWT merely because the token is difficult to read at a glance.
25. Failing to Rotate Compromised Credentials
Deleting an exposed API key from source code does not make the original key safe. If a credential has been exposed, assume it may have been copied and rotate or revoke it.
The same principle applies to database credentials, signing keys, cloud credentials, access tokens, and other secrets. Secret rotation should be supported operationally rather than treated as an emergency-only procedure.
26. Logging Secrets
Secrets can leak through application logs even when they are never committed to source control. Authorization headers, cookies, password-reset tokens, API keys, and request bodies may be captured by debug logging or observability systems.
console.log("Request headers:", request.headers);Logging should be designed so that sensitive fields are removed, masked, or excluded. Debug logging that is acceptable in a local environment can become dangerous when enabled in production.
27. Using Predictable Security Tokens
Password-reset links, email verification tokens, session identifiers, API credentials, and other security-sensitive values need sufficient unpredictability.
const token = Math.random().toString(36).slice(2);General-purpose pseudo-random functions are not appropriate for generating security-sensitive secrets. Use a cryptographically secure random generator provided by the platform or runtime.
28. Confusing Encoding With Security
Base64, URL encoding, hexadecimal encoding, and similar transformations do not encrypt data. They change representation so that data can be transported or processed conveniently.
Original: secret
Base64: c2VjcmV0Anyone who can decode the value can recover the original content. Encoding should therefore never be used as a substitute for encryption, hashing, authentication, or access control.
29. Using Security Through Obscurity as the Main Defense
Changing an endpoint from /admin to an unusual path, hiding an API route from navigation, or using an obscure parameter name does not replace authentication and authorization.
Unusual URLs can reduce accidental discovery, but security-sensitive operations still need explicit access controls. Assume attackers can discover endpoints through source code, network traffic, documentation, or automated scanning.
30. Forgetting Security During Deployment
An application can be secure in development and become less secure in production because of infrastructure configuration. Debug mode, public administration panels, exposed environment variables, permissive firewall rules, development credentials, or missing HTTPS configuration can create serious problems.
- Disable development and debug features in production.
- Review publicly exposed ports and services.
- Use production-specific secrets.
- Verify HTTPS and TLS configuration.
- Restrict administrative interfaces.
- Review cloud storage permissions.
- Check production environment variables.
- Monitor authentication and security events.
How to Prevent Security Mistakes During Development
Security is easier to maintain when it is incorporated into the development process instead of being treated as a final audit. Reusable authentication middleware, centralized authorization checks, validated request schemas, secure defaults, dependency scanning, and automated tests can prevent entire classes of mistakes.
Code review should also include security questions. Reviewers can ask whether user input is trusted, whether authorization is checked against the resource, whether secrets can reach the client, whether sensitive information is returned, and whether an endpoint can be abused through repeated requests.
Security Should Be Enforced on the Server
Frontend code is useful for user experience, but it should not be the final security boundary. A hidden button, disabled form field, frontend route guard, or client-side permission check can be bypassed by directly sending a request to the server.
if (!currentUser.permissions.includes("delete:users")) {
return new Response("Forbidden", { status: 403 });
}
await deleteUser(userId);The server should make the final decision about whether an operation is permitted. Frontend checks can still improve the interface, but they should be treated as convenience rather than protection.
Use Secure Defaults
Security improves when the safe option is the default option. Examples include secure cookies, HTTPS, parameterized database queries, restrictive CORS policies, validated request schemas, disabled debug output, and server-side authorization.
Secure defaults reduce the number of security decisions developers need to remember for every individual feature. When an exception is necessary, it becomes explicit and easier to review.
Separate Public and Private Configuration
Modern frontend frameworks often expose some environment variables to browser code intentionally. This makes it important to understand which variables are public and which must remain server-side.
| Configuration | Browser-visible? |
|---|---|
| API base URL | Usually acceptable if the API is intended to be public. |
| Public analytics ID | Usually acceptable when designed for client use. |
| Private API key | No |
| Database password | No |
| JWT signing secret | No |
| Private certificate key | No |
| Server encryption key | No |
Build a Security Review Checklist
A repeatable checklist helps prevent security review from depending entirely on memory. The exact checklist should match the application, but the following areas are useful for most web projects.
- Authentication and account recovery
- Authorization and resource ownership
- Password storage
- Session and cookie security
- Input validation
- Output encoding
- XSS protection
- CSRF protection
- SQL and command injection prevention
- CORS configuration
- Security headers
- HTTPS and TLS
- API key and secret management
- Rate limiting
- File upload handling
- Error handling
- Logging and monitoring
- Dependency security
- Production configuration
- Credential rotation
What Developers Should Check Before Production
- No production secrets are committed to the repository.
- No private credentials are exposed to browser code.
- HTTPS is enabled across the application.
- Obsolete TLS versions are disabled.
- Authentication is protected against common abuse.
- Authorization is checked server-side.
- Passwords use an appropriate password-hashing algorithm.
- Session cookies use appropriate security attributes.
- User input is validated on the server.
- HTML output is safely encoded or sanitized.
- Database queries use parameterization.
- CORS allows only required origins.
- CSRF protections match the authentication architecture.
- Security headers are configured appropriately.
- Sensitive fields are excluded from API responses.
- Production errors do not expose internal details.
- Rate limits exist for abuse-sensitive operations.
- File uploads are restricted and validated.
- Dependencies are monitored for security issues.
- Sensitive values are excluded from logs.
- Credential rotation procedures are available.
Frequently Asked Questions
What is the most common web security mistake?
There is no single mistake that applies to every application. Missing authorization checks, unsafe handling of untrusted input, exposed secrets, weak authentication, and insecure configuration are recurring classes of problems. The relevant risks depend on the application's architecture and data.
Is client-side validation a security feature?
Client-side validation is useful for user experience but should not be considered a security boundary. Attackers can bypass browser code and send requests directly to the server, so security-sensitive validation must also happen on the server.
Can a frontend application safely contain an API key?
Only if the key is intentionally public and the provider designed it for browser use. A secret API key should not be included in frontend JavaScript because users can inspect the application and recover the value.
Does HTTPS protect a website from hacking?
HTTPS protects data exchanged over the TLS connection and helps authenticate the server, but it does not prevent vulnerabilities such as SQL injection, broken authorization, XSS, or insecure dependencies. HTTPS is one security layer rather than a complete security solution.
Is CORS a security mechanism?
CORS is a browser mechanism controlling whether web pages can read certain cross-origin responses. It is not a replacement for authentication or authorization, and a server must enforce access control independently of its CORS configuration.
Should JWT payloads contain passwords or secrets?
No. JWT payloads are normally encoded rather than encrypted, so their contents should be considered readable by whoever possesses the token. Tokens should contain only information appropriate for the application's authentication model.
What should I do if an API key was accidentally exposed?
Treat the key as compromised. Revoke or rotate it, investigate where it was exposed, remove the exposed value from active configuration and source where appropriate, and review logs or provider activity for suspicious use.
Helpful Web Security Tools
A Security Headers Generator can help review and construct common browser security headers, while a CSP Header Builder is useful when developing a Content-Security-Policy. An HSTS Header Generator can help create Strict-Transport-Security configurations. A JWT Inspector is useful for inspecting JWT headers and claims during development, and an API Key Generator can create random API credentials for applications that need them. Sensitive production credentials should never be pasted into third-party tools unless their security and data-handling model is appropriate for that material.
Conclusion
Most web security mistakes come from trusting something that should have been treated as untrusted, enforcing a security rule only in the frontend, exposing sensitive information, or relying on a configuration that was never properly reviewed.
Strong web security comes from multiple layers: secure authentication, explicit authorization, safe input handling, output encoding, protected secrets, secure sessions, HTTPS, appropriate security headers, rate limiting, dependency maintenance, and careful production configuration.
The most useful habit is to make security part of normal development rather than a final step before release. When secure defaults, server-side checks, automated validation, secret management, and regular reviews are built into the development process, many common vulnerabilities can be prevented before they reach production.