Skip to main content
Play · Sandbox

Security Sandboxes

You cannot learn to defend an application without seeing one broken, and you must never practise on something you do not own. Everything below is either deliberately vulnerable software you run yourself or a lab whose owners invite you in - which is what makes it legal as well as useful.

What you practise here

  • The OWASP Top 10, one category at a time
  • Token validation: signature, audience, expiry
  • Injection, and why parameters fix it
  • Reading a dependency advisory and acting on it

Notebooks & hosted labs

Environments that run in a browser tab or in one command on your own machine. Nothing here needs a licence.

Starter code samples

Short enough to read in one go and to paste into a cell. Change them until they break — that is the exercise.

Verifying a signature is not verifying a token

python

A valid signature only proves somebody with the key minted it. Without the audience and expiry checks, a token for another service and a token from last year both sail through.

import jwt

claims = jwt.decode(
    token,
    key=PUBLIC_KEY,
    # Never read the algorithm from the token's own header: that is how
    # an attacker re-signs it with "alg": "none" or with HMAC.
    algorithms=["RS256"],
    audience="cyberscallywags.org",
    issuer="https://auth.cyberscallywags.org",
    options={"require": ["exp", "iat", "aud", "iss"]},
)
# jwt.ExpiredSignatureError / InvalidAudienceError are raised, not returned -
# there is no path where an invalid token becomes an empty dict you forget to check.

Injection is a string-formatting bug

python

Every injection - SQL, Cypher, shell, LDAP - is the same mistake: data concatenated into a program. Parameters keep the two apart.

# Never. An email of "' OR true //" now returns every member.
await session.run(
    f"MATCH (u:User) WHERE u.email = '{email}' RETURN u"
)

# Always. $email travels beside the query and can never be parsed as Cypher.
await session.run(
    "MATCH (u:User {email: $email}) RETURN u.display_name AS name",
    email=email,
)

Hash passwords with something built for it

python

SHA-256 is fast, and fast is the wrong property. Argon2 is deliberately slow and salts every hash for you - the same choice this platform makes.

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

hasher = PasswordHasher()
stored = hasher.hash("correct horse battery staple")

try:
    hasher.verify(stored, submitted_password)
except VerifyMismatchError:
    # Same message and roughly the same timing as an unknown account:
    # a different answer tells an attacker which emails are registered.
    raise InvalidCredentials("Email or password is incorrect")

Free practice grounds

Places to keep going once the samples run. Every one is usable without paying; where an account is needed we have said so.

  • Thirty-odd levels over SSH that teach the Linux command line by making you earn the next password. The best first wargame there is.

    OverTheWire
  • A capture-the-flag built for beginners, with every past year left online to play. Our coding nights run these as a group.

    Carnegie Mellon University
  • Guided rooms with a browser-based machine attached. The free tier covers the whole beginner path.

    TryHackMe
  • Short lessons on one vulnerability class each, written for developers fixing code rather than for testers finding it.

    Snyk

Reference & documentation

What to read when the sandbox stops answering the question.

Keep going with the community

The links above belong to the wider web. These ones belong to us — written, reviewed and taught by members.

Other sandboxes