An educational web application security training platform built around the OWASP Top 10.
Learn to write secure code by studying vulnerable and secure implementations side by side.
SecureLearn is a hands-on learning platform that teaches web application security. Each topic isolates one vulnerability class and walks through how it arises, how it's exploited, and — most importantly — how to prevent it, with a signature vulnerable-vs-secure code comparison, an attack-flow diagram, a real-world case study, a quiz, and practice exercises.
⚠️ This is a teaching tool, not a target. The vulnerable snippets are inert illustrations shown for study. SecureLearn ships no runnable exploitable endpoints, and the application itself is built to model the secure practices it teaches.
- 📚 12 in-depth modules covering the OWASP Top 10 vulnerability classes
- 🔴🟢 Vulnerable-vs-secure diptych — the core teaching device, on every module
- 🧭 Attack-flow diagrams rendered with Mermaid
- 🏛️ Real-world case studies (Heartland, the Samy worm, Capital One, LinkedIn, Firesheep, …)
- 📝 48 quiz questions with per-question explanations and scoring
- 📊 Progress tracking — completion ring, per-module status, quiz stats, activity feed
- 🔎 Ranked search across titles, tags, OWASP ids and body text
- 🌗 Light / dark theme, responsive, keyboard-accessible, reduced-motion aware
- 🔌 Read-only JSON API reusing the same content + service layer
- 🐳 Docker & Docker Compose, ✅ 194 tests / 96% coverage, 🔁 GitHub Actions CI
- 🗄️ Versioned schema — Flask-Migrate / Alembic, verified on SQLite and PostgreSQL
- 🚦 Rate limiting on the credential-guessing surfaces (Flask-Limiter)
- 📈 Ops-ready — liveness/readiness probes, JSON logs with request ids, optional Prometheus metrics
| Control | Implementation |
|---|---|
| Parameterised queries | SQLAlchemy ORM everywhere |
| Salted password hashing | User.set_password (Werkzeug) |
| CSRF protection on all forms | Flask-WTF CSRFProtect |
State changes never on GET |
POST /logout, CSRF-protected like every other mutation |
| Deny-by-default authorization | security.role_required |
| Security headers + strict CSP | security.apply_security_headers |
| HSTS on HTTPS responses only | security.apply_security_headers (never advertised over plain http) |
| Output escaping | Jinja2 autoescape on all user content |
| Open-redirect defence | auth._safe_next |
| Session regeneration on login | session.clear() in auth.login |
| Subresource Integrity on CDN assets | integrity + crossorigin on every third-party script/style |
| Fail-fast on insecure config | _validate_config refuses a known SECRET_KEY in production |
| No published credentials in prod | Seeding refuses the default admin password; demo account is off |
| Forged proxy headers untrusted | X-Forwarded-* honoured only when TRUSTED_PROXY_COUNT is set |
Nonce-based CSP (no unsafe-inline scripts) |
security.build_csp — a fresh 128-bit nonce per request |
| Brute-force resistance | Flask-Limiter on login, registration, password change, search |
| Errors leak nothing | Generic messages to the client; the detail goes to the log |
| Runs unprivileged in Docker | USER appuser — never root |
git clone https://github.com/Joshwa-jd/securelearn.git
cd securelearn
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python run.py # http://127.0.0.1:5000 (auto-creates & seeds the DB)Demo accounts (development only): learner / Learner!2345 · admin admin / Admin!2345
These are seeded for local exploration and their passwords are published here, so
production refuses them: the demo account is not created under
FLASK_CONFIG=production, and seeding aborts while SEED_ADMIN_PASSWORD is
still the default.
Production config requires real secrets — the app fails fast rather than boot
with a known key, so supply them via a .env file (compose reads it) or the
shell:
cp .env.example .env
# set SECRET_KEY and SEED_ADMIN_PASSWORD to strong, unique values
# python -c "import secrets; print(secrets.token_hex(32))"
docker compose up --build # served by gunicorn on http://localhost:8000Set WEB_PORT if 8000 is already taken. Behind a TLS-terminating reverse proxy,
also set SESSION_COOKIE_SECURE=true and TRUSTED_PROXY_COUNT=1 so HTTPS is
detected and HSTS is sent.
The container runs as an unprivileged user, applies migrations on start-up, and
exposes /api/health/live and /api/health/ready for orchestrators. See the
deployment guide for the full production path.
| Module | OWASP | Level | Time |
|---|---|---|---|
| SQL Injection | A03:2021 Injection | Beginner | 25 min |
| Cross-Site Scripting (XSS) | A03:2021 Injection | Beginner | 25 min |
| Output Encoding | A03:2021 Injection | Intermediate | 20 min |
| Input Validation | A03:2021 Injection | Beginner | 20 min |
| Cross-Site Request Forgery (CSRF) | A01:2021 Broken Access Control | Intermediate | 20 min |
| Insecure Direct Object References (IDOR) | A01:2021 Broken Access Control | Intermediate | 20 min |
| Authorization & Access Control | A01:2021 Broken Access Control | Intermediate | 20 min |
| Authentication Security | A07:2021 Identification & Authentication Failures | Intermediate | 25 min |
| Session Security | A07:2021 Identification & Authentication Failures | Intermediate | 20 min |
| Server-Side Request Forgery (SSRF) | A10:2021 SSRF | Advanced | 25 min |
| File Upload Security | A04:2021 Insecure Design | Advanced | 25 min |
| Security Headers | A05:2021 Security Misconfiguration | Beginner | 20 min |
Every module page includes: Overview · How it works · Attack flow · Vulnerable vs secure code · Common mistakes · Prevention · OWASP mapping · Case study · Exercises · Quiz · References.
A module page (dark theme) — attack flow, the vulnerable-vs-secure diptych, OWASP mapping, case study and quiz.
The learner dashboard — completion ring, per-module status and activity feed.
These are captured from a running instance so they always match the code. To
regenerate them (light and dark, every page), see
docs/screenshots/ for the Playwright script.
Backend: Python 3.11–3.13, Flask 3, Flask-SQLAlchemy, Flask-Login, Flask-WTF,
Flask-Migrate, Flask-Limiter, SQLAlchemy 2, gunicorn
Frontend: Jinja2, Bootstrap 5.3, Bootstrap Icons, Mermaid, a bespoke CSS
token layer, vanilla JS (theme toggle)
Data: SQLite by default, PostgreSQL for production (any SQLAlchemy URL via
DATABASE_URL); schema versioned with Alembic
Ops: structured JSON logging, liveness/readiness probes, optional
Prometheus metrics
Tooling: pytest, pytest-cov, flake8, Bandit, pip-audit, Docker, Docker
Compose, GitHub Actions
securelearn/
├── app/
│ ├── __init__.py # application factory
│ ├── config.py # environment-driven config
│ ├── models.py # SQLAlchemy models
│ ├── forms.py # WTForms
│ ├── security.py # role_required · headers · CSP nonce · markdown
│ ├── services.py # progress & dashboard logic
│ ├── seed.py # migrate + default users (idempotent)
│ ├── logging_config.py # structured logging + request correlation ids
│ ├── monitoring.py # health probes + Prometheus metrics
│ ├── extensions.py # db · login · csrf · migrate · limiter singletons
│ ├── blueprints/ # main · auth · learning · api
│ └── content/ # schema + registry + 12 module files (typed data)
├── migrations/ # Alembic environment + versioned revisions
├── templates/ # base · partials · pages · errors
├── static/ # css/style.css · js/main.js
├── tests/ # pytest suite (194 tests)
├── scripts/smoke_test.py # black-box check of a running instance (used by CI)
├── docker/Dockerfile
├── docker-compose.yml
├── .github/workflows/ci.yml
├── run.py # dev entrypoint
└── wsgi.py # gunicorn entrypoint
pip install -r requirements-dev.txt
pytest # 194 tests
pytest --cov=app # coverage (96%)
flake8 # lint — scope comes from setup.cfg
bandit -q -r app scripts # static analysis
pip-audit -r requirements.txt # dependency advisoriesEvery push and pull request runs, in parallel:
| Job | What it proves |
|---|---|
| Lint | flake8 is clean |
| Test (3.11 · 3.12 · 3.13) | 194 tests pass; coverage stays ≥ 90% |
| Test (PostgreSQL) | the same suite passes on the production engine |
| Migrations (SQLite · PostgreSQL) | upgrade → flask db check → downgrade → upgrade again |
| Security scan | Bandit + pip-audit |
| Docker | the image builds, boots, serves, and runs as non-root |
| Compose | the documented docker compose up path actually works |
The Docker and Compose jobs smoke-test a running container over HTTP — health, readiness, liveness, the API contract, security headers and per-request CSP nonces — parsing JSON rather than grepping it, because production serves compact JSON and development does not.
| Doc | Contents |
|---|---|
| Installation | Local + Docker setup, configuration |
| Deployment | Production: secrets, PostgreSQL, migrations, probes, proxy, rollback |
| Architecture | Layers, request lifecycle, data model, diagrams |
| API Reference | JSON API endpoints and examples |
| Developer Guide | Project layout, adding modules/routes, migrations |
| User Guide | How to use the platform |
| Future Improvements | Roadmap ideas |
| Contributing | How to contribute |
| Security Policy | Reporting a vulnerability, threat model, scope |
| Changelog | Release history |
The content is authored as typed Python dataclasses rather than scattered across templates. This makes every module unit-testable, lets a single template render all of them consistently, and allows search to index every field. Adding a module is a single new file — no route or template changes required.
v1.0.0 is feature-complete. Ideas under consideration, roughly in order:
- More modules — XXE, insecure deserialization, SSTI, dependency/supply-chain security, cryptographic failures, logging & monitoring failures
- Learning paths — curated sequences (e.g. "Injection track") with prerequisites
- Spaced repetition — resurface quiz questions a learner previously got wrong
- Admin authoring UI — manage content without editing Python files
- i18n — externalise strings and translate the module catalogue
- Redis-backed rate limiting by default for multi-worker deployments
See docs/future-improvements.md for the fuller
list, including the trade-offs behind each. Issues and proposals are welcome.
Contributions are welcome — bug reports, new modules, docs fixes and accessibility improvements especially.
pip install -r requirements-dev.txt
pytest && flake8 && bandit -q -r app scriptsPlease read CONTRIBUTING.md for the workflow (branching, migrations, adding a module) and the Code of Conduct. Security issues should not be filed as public issues — follow SECURITY.md instead.
MIT © 2026 Joshwa J D
Built as a portfolio project to demonstrate secure application development and the OWASP Top 10. For learning and authorised testing only.
