Skip to main content
PProfileMatch

OWASP Top 10: Vulnerable Components, Insecure Design, SSRF and Logging

CybersecurityLesson 3 of 46 min read

What you'll learn

  • Explain how vulnerable and outdated components enter a project and how to track them
  • Distinguish insecure design from implementation bugs and fix a business-logic race
  • Describe server-side request forgery and validate outbound destinations defensively
  • Design useful security logging without recording secrets

On this page

  1. Vulnerable and outdated components
  2. Insecure design
  3. Server-side request forgery (SSRF)
  4. Logging and monitoring failures
  5. Common mistakes
  6. How this shows up in real projects and interviews

A bookmarking app has a handy feature: paste a link and the server fetches the page title and a preview image. The same app lets each new user redeem a welcome coupon, and it logs only the word "error" when something fails. Four themes from the OWASP Top 10 (current edition) live in that one description. The server will fetch whatever address it is given. The coupon logic may be sound in code and unsound in design. The framework underneath may be years behind on patches. And when something goes wrong, nobody will be able to tell.

Vulnerable and outdated components

Modern applications are mostly other people's code. A typical project pulls in hundreds of transitive dependencies, each of which can have a published vulnerability. The risk is not only the library you chose; it is the ones that came along with it, and the base images, runtimes and plugins around it. The remedy is a routine, not a heroic audit.

  • Know what you run: keep an inventory of direct and transitive dependencies and the versions in production, ideally as a generated software bill of materials.
  • Scan continuously: use the audit tool for your ecosystem in your pipeline, such as npm audit, pip-audit or your build tool's dependency check, and a service that opens upgrade pull requests.
  • Upgrade in small steps: frequent minor upgrades are easier to test than one jump after three years.
  • Remove what you do not use: every unused dependency is attack surface with no benefit.
  • Pin and verify: lock files and checksum verification protect against surprise changes.

Not every reported vulnerability matters equally. A flaw in a function you never call, or one that needs a configuration you do not use, can be scheduled; one in an internet-facing parsing path should be patched quickly. Triage by reachability and exposure, and write down the reason when you defer.

Insecure design

Some flaws are not coding slips. A perfectly written function can implement an unsafe idea: a password reset that asks "what is your pet's name?", a booking flow with no limit per user, a discount rule that can be stacked. Insecure design is fixed earlier, with threat modelling (see the CIA triad and threat modelling), abuse-case thinking and agreed security requirements, not by tidier code.

A concrete case is a check-then-act sequence. Two simultaneous requests can both pass a "does this coupon still have a use left?" check before either one records the use. The design fix is to make the decision atomic, in one statement the database resolves for you.

coupon.py
import sqlite3

db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE coupons (code TEXT PRIMARY KEY, uses_left INTEGER NOT NULL CHECK (uses_left >= 0));
INSERT INTO coupons VALUES ('WELCOME', 1);
""")

def redeem_check_then_act(code: str) -> bool:
    # Two steps: two concurrent requests can both pass the check before either writes.
    row = db.execute("SELECT uses_left FROM coupons WHERE code = ?", (code,)).fetchone()
    if row and row[0] > 0:
        db.execute("UPDATE coupons SET uses_left = uses_left - 1 WHERE code = ?", (code,))
        return True
    return False

def redeem_atomic(code: str) -> bool:
    # One statement: the database decides, and rowcount tells us whether it won.
    cur = db.execute(
        "UPDATE coupons SET uses_left = uses_left - 1 WHERE code = ? AND uses_left > 0",
        (code,),
    )
    return cur.rowcount == 1

print("first redeem :", redeem_atomic("WELCOME"))
print("second redeem:", redeem_atomic("WELCOME"))

Output

first redeem : True
second redeem: False

The demo runs sequentially, so it cannot show the race itself; what it shows is the safe shape. The first function leaves a window between the read and the write. The second has no window, and the CHECK constraint on the column is a second safety net that makes the database refuse a negative balance outright.

Server-side request forgery (SSRF)

In the link preview feature, the server makes a request on behalf of the user. SSRF occurs when the user controls the destination and the server will happily reach places the user cannot: internal admin pages, databases with no authentication because they sit "inside the network", or cloud provider metadata services that hand out credentials to the machine. The server becomes a confused deputy.

Defences are layered. Allow only the schemes you need (usually http and https). Resolve the hostname and refuse private, loopback, link-local and reserved addresses. Prefer an allow-list of destinations when the feature permits one. Run fetchers in a separate network segment with no route to internal services, apply a short timeout and response-size limit, do not follow redirects blindly (a redirect can point inward), and use the cloud provider's hardened metadata settings. The function below shows the address-checking idea.

preview_guard.py
import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_SCHEMES = {"http", "https"}

def is_safe_preview_target(url: str) -> bool:
    parts = urlparse(url)
    if parts.scheme not in ALLOWED_SCHEMES or not parts.hostname:
        return False
    try:
        infos = socket.getaddrinfo(parts.hostname, parts.port or 443)
    except socket.gaierror:
        return False
    for info in infos:
        addr = ipaddress.ip_address(info[4][0])
        if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved:
            return False
    return True

for candidate in [
    "http://127.0.0.1:8080/admin",
    "http://169.254.169.254/latest/meta-data/",
    "file:///etc/passwd",
    "http://10.0.0.5/",
    "http://192.168.1.10/status",
    "http://8.8.8.8/",
]:
    print(candidate, "->", is_safe_preview_target(candidate))

Output

http://127.0.0.1:8080/admin -> False
http://169.254.169.254/latest/meta-data/ -> False
file:///etc/passwd -> False
http://10.0.0.5/ -> False
http://192.168.1.10/status -> False
http://8.8.8.8/ -> True

Logging and monitoring failures

Without logs, a breach is discovered by customers rather than by you. Record security-relevant events: logins and failures, access-denied decisions, password and role changes, and administrative actions, each with who, what, when and from where. Use a structured format so searches and alerts are reliable, centralise the logs off the machine that produced them, and set up at least a few alerts that would actually wake someone. Equally important is what you leave out: never log passwords, tokens, full card numbers or session ids, and mask personal data where you can.

audit_log.py
import json
import logging
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")
log = logging.getLogger("audit")

def audit(event: str, **fields) -> None:
    # One JSON object per line is easy to search and alert on.
    log.info(json.dumps({"event": event, **fields}, sort_keys=True))

def redact_email(email: str) -> str:
    name, _, domain = email.partition("@")
    return f"{name[:1]}***@{domain}"

audit("login_failed", user=redact_email("priya@example.com"), ip="203.0.113.9", attempt=4)
audit("access_denied", user="asha", resource="bookmark:2", reason="not_owner")
audit("password_changed", user="bruno", notified=True)

Output

{"attempt": 4, "event": "login_failed", "ip": "203.0.113.9", "user": "p***@example.com"}
{"event": "access_denied", "reason": "not_owner", "resource": "bookmark:2", "user": "asha"}
{"event": "password_changed", "notified": true, "user": "bruno"}

Common mistakes

  • Fixing only the dependency named in the alert. Symptom: the scanner is quiet but a transitive copy of the old version remains in the lock file. Fix: check the resolved tree, not just your manifest.
  • Validating a URL string and then fetching it. Symptom: a hostname that passes the check still reaches an internal address after redirects or DNS changes. Fix: validate the resolved address at connect time and on every hop.
  • Logging the whole request body. Symptom: passwords and tokens appear in a log search, so the log store becomes the most valuable target. Fix: allow-list the fields you log.
  • Collecting logs but never alerting. Symptom: weeks of failed logins noticed after the fact. Fix: at least one alert per high-risk event type, tested by triggering it.
  • Treating design flaws as "later". Symptom: rate limits and abuse rules bolted on after launch because the data model cannot support them. Fix: write abuse cases during design.

How this shows up in real projects and interviews

Component risk is where most organisations are actually exposed, and it is a process problem: someone has to own dependency updates. SSRF has become a popular interview topic because it connects web code with cloud networking, so be ready to explain a layered answer rather than a single regex. Insecure design questions often arrive as "what could go wrong with this feature?", and the best answers walk through abuse by a legitimate user, not just by an outsider. For where updates and scans fit in an automated pipeline, see CI/CD.

Try it yourself

Pick one outbound feature in an app you know (webhooks, link previews, image import by URL). Write the allowed schemes and destinations, the timeout and size limits, what happens on redirect, and which log line would show someone trying to reach an internal address.

Show a hint

Include a rule for redirects and one for DNS resolution, and decide whether an allow-list of domains is possible. A feature that only ever talks to three partner domains should not accept arbitrary URLs.

Key takeaways

  • Vulnerable components are managed by an ongoing routine: inventory, scan, upgrade in small steps, remove the unused.
  • Insecure design is fixed in requirements and threat modelling; make critical decisions atomic and enforce them in the data layer.
  • SSRF turns your server into a proxy; restrict schemes, destinations, redirects and network reach.
  • Log security events in a structured format, centralise them, alert on a few important patterns and never log secrets.
  • Triage vulnerabilities by reachability and exposure, and record the reason for any deferral.

Test yourself

A trick from this lesson's ride: In SSRF your server is the courier, walking through doors you could never open. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What is server-side request forgery (SSRF)?

SSRF is when an attacker makes your server send requests to a destination of their choosing, often internal services or cloud metadata endpoints that the attacker cannot reach directly. It is prevented by restricting destinations, schemes, redirects and the server's network access.

How do I check my dependencies for known vulnerabilities?

Run your ecosystem's audit command in CI (for example npm audit or pip-audit), enable automated dependency update pull requests, and check the resolved lock file, not just the packages you named directly.

What is the difference between insecure design and a coding bug?

A coding bug is a mistake in implementing a correct idea. Insecure design is an idea that is unsafe even if implemented perfectly, such as unlimited coupon use or weak recovery questions. Design flaws need changes to requirements and architecture, not just a patch.