Security

Common Web Vulnerabilities: Findings from Real Security Audits

Every security audit reveals the same patterns. These are the vulnerabilities that matter and the practical fixes that actually prevent breaches.

SQL injection is not dead—it is just hiding in newer stacks

SQL injection happens when user input is directly concatenated into SQL queries. An attacker enters a payload like " OR 1=1 --" which changes the query logic and returns all data instead of one record.

Modern frameworks have mostly eliminated raw SQL injection by default, but the vulnerability lives on in several forms:

First, old code often still uses string concatenation for queries. If you inherited a codebase or use legacy systems, this is worth auditing.

Second, NoSQL databases (MongoDB, etc.) have their own injection vulnerabilities. The patterns are different, but the outcome is the same—attackers can extract data by manipulating queries.

Third, SQL injection appears in unusual places. Database names, table names, and column names cannot be parameterized, so teams sometimes concatenate them. This opens the door to injection.

The fix is always the same: use parameterized queries. Pass user input as parameters separate from the query string. The database driver handles escaping correctly.

  • Always use parameterized queries or prepared statements.
  • Never concatenate user input into queries.
  • Use ORMs that generate parameterized queries by default.
  • Test for injection by trying payloads like " OR 1=1 --" in every input.
  • Log suspicious queries for detection even if injection is prevented.

Authentication bypass through session manipulation

Session tokens are how web applications prove that a request is from an authenticated user. A compromised or predictable session token is an instant authentication bypass.

Common mistakes: sessions stored in unencrypted cookies, sessions with weak randomness (predictable tokens), sessions that never expire, sessions sent over HTTP (even once).

We often find session tokens that are sequential numbers, timestamps, or other easily guessable values. An attacker can increment the session ID and impersonate any user.

The fix is using cryptographically strong session tokens generated by the framework. Tokens should be at least 128 bits of random data. They should be transmitted only over HTTPS. They should expire after a reasonable timeout.

Validate session tokens on every request. Do not trust the browser to enforce permissions. If a session token is invalid or expired, make the user log in again.

Also implement secure logout. Invalidate the session server-side so that a stolen token cannot be replayed after logout.

Cross-site scripting (XSS) through unescaped user content

XSS happens when user-provided content is rendered as code instead of text. An attacker injects JavaScript that runs in other users browsers.

The most common variant is stored XSS: an attacker posts a comment with JavaScript embedded, and everyone who reads the comment executes that JavaScript. This can steal cookies, session tokens, or redirect users to malicious sites.

The fix is escaping user input when rendering it. If a user submits "<script>alert(1)</script>", it should render as literal text, not as executable code.

Modern frameworks handle escaping by default. React escapes by default. If you are using a framework that does not escape by default, you are at risk.

The one exception is rich text editors where you intentionally allow HTML. In that case, use a whitelist of allowed tags and strip dangerous ones like <script>. Tools like DOMPurify handle this safely.

Broken access control—users can access data they should not

This is the most common vulnerability we find: a user can access data that should be restricted to other users.

Common mistakes: not checking permissions server-side, trusting the client to enforce permissions, using predictable IDs (user 1, 2, 3... just try incrementing and see what you can access).

A user should not be able to access another user is data by changing a URL parameter or request ID. They should not be able to access admin functions. They should not be able to escalate their own permissions.

The fix is checking permissions server-side on every request. If a user requests data, verify that they own that data or have permission to access it. Do this consistently across all endpoints.

Use framework tools for this. Most frameworks have middleware or decorators for authorization checks. Use them.

Insecure dependencies with known vulnerabilities

Applications pull in hundreds of dependencies, and many have known vulnerabilities. Attackers scan for applications using outdated, vulnerable dependencies.

We find this constantly: npm packages with critical vulnerabilities that have patches available, libraries that are years out of date.

The fix is using dependency scanning tools. npm audit, Dependabot, Snyk all scan dependencies and alert you to vulnerabilities. Most integrate into CI/CD so vulnerable packages are caught before deployment.

Update regularly. Set up automatic updates for patch versions. Review breaking changes for major versions and update periodically.

Also audit your dependencies for necessity. Remove packages you do not use. Each dependency is attack surface.

Credentials in code and environment variables exposed

We regularly find API keys, database passwords, and OAuth tokens committed to version control or exposed in build artifacts.

A single exposed AWS key can cost thousands in compute charges or expose entire databases.

Never commit secrets to code. Use environment variables for local development. Use a secrets management system (AWS Secrets Manager, HashiCorp Vault, GitHub Secrets) for production.

Rotate credentials regularly. If a credential is exposed, changing it limits the damage window.

Scan repositories for secrets before they are pushed. Tools like TruffleHog detect common patterns (AWS keys, private keys, etc.) and prevent commits that contain them.

Bottom Line

These are the kinds of decisions that separate systems that age well from systems that become expensive to change.

← Back to blog