3 min read
On this page

Injection & Broken Access Control

The Two Most Common Web Vulnerabilities

The OWASP Top 10 (2021 edition) ranks Broken Access Control as A01 and Injection as A03. Together, these two categories account for more breaches than any other web vulnerability class. They are also among the most preventable.

A01: Broken Access Control

Access control enforces policy such that users cannot act outside their intended permissions. When it fails, unauthorized users can view, modify, or delete data they should not have access to.

Insecure Direct Object References (IDOR)

IDOR occurs when an application exposes internal object references (database IDs, file paths) and does not verify that the requesting user is authorized to access that object.

Vulnerable endpoint:
  GET /api/invoices/12345

The application returns invoice 12345 without checking whether
the authenticated user owns that invoice. An attacker simply
changes the ID:

  GET /api/invoices/12346
  GET /api/invoices/12347
  ...

They can enumerate every invoice in the system.
Real example: 2018 Facebook photo API IDOR
- Facebook's Photo API allowed apps to access photos
  using the photo ID without verifying ownership
- Attackers could access unposted photos, marketplace photos,
  and Stories by iterating through IDs
- 6.8 million users' private photos exposed
- Root cause: missing authorization check on the photo object

Fix for IDOR

Bad — no authorization check:
  invoice = db.query("SELECT * FROM invoices WHERE id = ?", invoice_id)
  return invoice

Good — verify ownership:
  invoice = db.query(
    "SELECT * FROM invoices WHERE id = ? AND user_id = ?",
    invoice_id, current_user.id
  )
  if not invoice:
    return 404

Even better — use UUIDs instead of sequential integers:
  GET /api/invoices/a8f5f167-f44d-4e52-b90b-cd42fb7a76c4
  (UUIDs are not guessable, but still check authorization)

Missing Function-Level Access Control

Some applications check authorization on the frontend but not the backend.

Scenario:
- Admin panel button only renders for admin users in the UI
- But the API endpoint /api/admin/users has no server-side check
- Any authenticated user who discovers the endpoint can use it

This is not theoretical. It is one of the most common findings
in penetration tests. Frontend checks are for UX, not security.
Prevention:
- Check authorization on every request at the server
- Use middleware or decorators that enforce role checks
- Default deny: if no access rule matches, deny access
- Test with low-privilege accounts hitting high-privilege endpoints

Privilege Escalation

Vertical escalation: Regular user gains admin access
  - Changing a role parameter: POST /api/profile { "role": "admin" }
  - Accessing admin endpoints without role verification
  - Modifying JWT claims without server-side validation

Horizontal escalation: User A accesses User B's data
  - IDOR on user-specific resources
  - Predictable session tokens or user identifiers
  - Missing tenant isolation in multi-tenant applications

Real example: 2019 First American Financial
- 885 million mortgage documents accessible via sequential URLs
- No authentication required at all
- Documents included SSNs, bank statements, tax records
- Simple URL manipulation: change the document number in the URL

Access Control Best Practices

1. Default deny: Start with no access, explicitly grant permissions
2. Check on every request: Never cache authorization decisions client-side
3. Use framework-level enforcement: Middleware, decorators, policies
4. Centralize access control logic: Don't scatter checks across handlers
5. Log access control failures: They indicate probing or attacks
6. Test with multiple roles: Automate tests that verify boundaries
7. Rate limit sensitive endpoints: Slow down enumeration attempts

A03: Injection

Injection occurs when untrusted data is sent to an interpreter as part of a command or query. The attacker's data tricks the interpreter into executing unintended commands or accessing unauthorized data.

SQL Injection

The most well-known injection type. User input becomes part of a SQL query.

Vulnerable code (pseudocode):
  query = "SELECT * FROM users WHERE name = '" + username + "'"
  db.execute(query)

Attack input:
  username = ' OR '1'='1' --

Resulting query:
  SELECT * FROM users WHERE name = '' OR '1'='1' --'

This returns all users. The -- comments out the rest of the query.
More dangerous payloads:
  ' UNION SELECT password FROM users --
  ' ; DROP TABLE users --
  ' ; INSERT INTO users (name, role) VALUES ('hacker', 'admin') --
Real example: 2017 Equifax breach (CVE-2017-5638)
- Apache Struts vulnerability allowed remote code execution
- Attackers exploited an injection flaw in the Content-Type header
- 147 million records stolen: SSNs, birth dates, addresses
- Patch had been available for two months before the breach
- Equifax failed to apply it to a single server

The Fix: Parameterized Queries

Parameterized query (safe):
  query = "SELECT * FROM users WHERE name = ?"
  db.execute(query, [username])

The database treats the parameter as data, never as SQL code.
No matter what the user inputs, it cannot change the query structure.

This is the fix. Not escaping. Not filtering. Parameterized queries.

NoSQL Injection

NoSQL databases are not immune to injection.

MongoDB vulnerable code:
  db.users.find({ username: req.body.username, password: req.body.password })

Attack payload (JSON body):
  { "username": "admin", "password": { "$ne": "" } }

The $ne (not equal) operator matches any non-empty password,
bypassing authentication entirely.

Fix: Validate input types. Username must be a string.
Use a schema validation library (Joi, Zod, etc.).

OS Command Injection

When user input is passed to system shell commands.

Vulnerable code:
  filename = request.get("file")
  os.system("convert " + filename + " output.pdf")

Attack input:
  file = "image.png; cat /etc/passwd"

Resulting command:
  convert image.png; cat /etc/passwd output.pdf

The semicolon terminates the first command and starts a new one.
Fix:
- Never pass user input to shell commands
- Use language-native libraries instead of shell commands
- If shell is unavoidable, use parameterized execution:
    subprocess.run(["convert", filename, "output.pdf"])
  This passes filename as an argument, not part of the command string

Prevention Summary

Injection type     | Prevention
-------------------|------------------------------------------
SQL injection      | Parameterized queries, ORMs (carefully)
NoSQL injection    | Input type validation, schema enforcement
Command injection  | Avoid shell; use language APIs; parameterize
LDAP injection     | Escape special characters, parameterized queries
XPath injection    | Parameterized XPath queries
Header injection   | Validate and sanitize header values

Combining the Threats

Broken access control and injection frequently compound each other.

Scenario: API with both vulnerabilities
1. Attacker finds SQL injection in search endpoint
2. Extracts admin credentials from the database
3. Uses admin credentials to access admin panel
4. Admin panel has no function-level checks on API
5. Attacker modifies data, creates backdoor accounts

If either vulnerability had been fixed, the full chain breaks.
This is defense in depth: multiple layers catching different failures.

Testing for These Vulnerabilities

Broken access control testing:
- Log in as User A, capture a request URL/body
- Replay the same request as User B
- If User B can see User A's data, you have IDOR
- Try accessing admin endpoints as a regular user
- Try modifying role/permission fields in requests
- Automate with tools: Burp Suite Authorize extension

Injection testing:
- Submit ' in every input field, look for SQL errors
- Submit ${7*7} to test template injection (expect 49)
- Submit ; id in fields that might reach shell commands
- Use SQLMap for automated SQL injection testing
- Check every input: URL params, headers, cookies, JSON bodies

Common Pitfalls

  • Relying on frontend authorization: Hiding buttons in the UI is not access control. Every authorization check must happen server-side. Attackers do not use your frontend.

  • Using sequential IDs without authorization checks: Auto-incrementing database IDs make enumeration trivial. Use UUIDs for external references, but always check authorization regardless of ID format.

  • Trusting ORMs to prevent all injection: ORMs prevent most SQL injection, but raw query methods bypass protection. Review every .raw(), .execute(), or string-interpolated query.

  • Only testing the happy path: If your tests only verify that authorized users can access resources, you are missing the critical test: that unauthorized users cannot.

  • Confusing authentication with authorization: Authentication verifies who you are. Authorization verifies what you can do. A system can authenticate users perfectly and still have broken access control.

  • Escaping instead of parameterizing: Escaping user input is error-prone and context-dependent. Parameterized queries are the correct solution for injection. Do not build your own escaping logic.

Key Takeaways

  • Broken Access Control (A01) is the most common web vulnerability. Check authorization on every request, server-side, with default deny.
  • IDOR is trivially exploitable: change an ID in a URL and see someone else's data. Always verify the requesting user owns the resource.
  • Injection (A03) happens when user input is interpreted as code. The fix is parameterized queries, not input filtering or escaping.
  • SQL injection caused the Equifax breach. The patch existed for months. Prevention is simple; the hard part is discipline.
  • Test for both vulnerabilities by replaying requests across user contexts and submitting injection payloads in every input.
  • These vulnerabilities are the most common because they are easy to introduce and easy to overlook. They are also the most preventable with basic engineering discipline.