Web Frameworks: Code Review
Overview — An offensive-path TryHackMe room on white-box testing: auditing an application when you already have its source code, instead of poking at it blindly from the outside. It teaches you to read code the way an attacker does — following the source-to-sink model — and applies that methodology to a real Flask target to find and exploit three vulnerabilities: SQL Injection, SSTI, and Path Traversal.
| Platform | TryHackMe |
| Category | Offensive — Secure Code Review |
| Difficulty | Medium |
| Room | Web Frameworks: Code Review |
Part 1 — Methodology
The core skill isn't memorising dangerous functions — it's reading code with intent: not top-to-bottom like a story, but asking «where does user input enter, and where does it eventually get used in a dangerous way?». Once you can answer that quickly for a codebase, finding real bugs stops being guesswork and becomes a repeatable process.
1.1 How to read an unfamiliar codebase
Opening random files wastes hours. The trick is to read in the order a real request flows through the app, so that by the time you reach the code that handles requests you already understand the context around it. Think of searching a building: you check the front door and the directory board before wandering into random rooms.
| Step | What you read | Why this order matters |
|---|---|---|
| 1 | README / Docs | Tells you what the app is supposed to do, so you can recognise when something doesn't fit that purpose. |
| 2 | Dependency manifest (requirements.txt) | A single outdated, pinned library version can be a known CVE — worth checking before reading a single line of app logic. |
| 3 | Configuration (config.py) | Settings like DEBUG = True or a hardcoded secret key change how every bug you find later should be treated. |
| 4 | Routing | This is literally the map of everything the outside world can touch. |
| 5 | Auth middleware | Tells you which routes need a login and which don't — huge for prioritising what to test first. |
| 6 | Database / Models | Shows you what «normal» data access looks like, so anything that breaks that pattern jumps out later. |
| 7 | Route handlers | The actual deep-dive — by now you already know what's public, what's protected, and what normal queries look like. |
In a Python/Flask project specifically: dependencies live in requirements.txt, routes are created with the @app.route decorator, and the debug flag plus secret key conventionally live in config.py.
1.2 The source-to-sink model
This is the single most useful idea in the whole room. Almost every injection bug boils down to two points connected by a broken (or missing) safety check:
- Source — where untrusted data enters the app. In Flask:
request.args(query string),request.form(form data),request.cookies, orrequest.json. - Sink — a function where that data, if unsanitised, causes real damage. Common ones:
cursor.execute()with raw string input → SQL Injectionsubprocess.run(..., shell=True)→ Command Injectionrender_template_string()on unvalidated input → SSTI
The workflow is simple in concept: start at a sink and walk backwards through the code until you either (a) find a source with no sanitisation in between — confirmed bug — or (b) hit a point where the data was properly validated or parameterised — safe. In the room's example, request.args.get("name") flows straight into render_template_string() with nothing in between, a textbook SSTI.
1.3 Letting tools do the first pass
Manually reading every line of a large app doesn't scale, so two tools do the heavy lifting before you ever touch a line by hand:
grep -rn "pattern" .— a dumb but fast text search.-rsearches every file recursively and-nprints the line number of each match, so you instantly get a clickable list of every place a dangerous function name appears.- Semgrep — smarter than grep because it understands code structure (via the AST), not just text. It can spot a dangerous pattern even if variable names or formatting differ. Pointing it at a ready-made ruleset uses the
--configflag:
semgrep --config p/owasp-top-ten .This checks the whole codebase against curated OWASP Top 10 rules in one command. The practical approach: grep first for speed, then Semgrep for coverage, and feed whatever both tools flag into the manual source-to-sink trace from 1.2.
1.4 The vulnerability classes in play
| Class | What actually causes it | Typical sink | CWE |
|---|---|---|---|
| SQL Injection | Building a query with an f-string or + instead of a parameterised / bound query | cursor.execute(f"...{input}...") | CWE-89 |
| Command Injection | shell=True hands the whole string to /bin/sh, so shell metacharacters (;, &&, backticks) get interpreted | subprocess.run(cmd, shell=True) | CWE-78 |
| SSTI | A user-influenced string gets compiled and executed as a template, instead of being passed as inert data into a static template | render_template_string(input) | CWE-1336 |
| Insecure Deserialization | Rebuilding Python objects from an untrusted byte stream lets a crafted object's __reduce__ method run arbitrary code the instant it's unpickled | pickle.loads(data) | CWE-502 |
| Path Traversal | Joining user input straight into a filesystem path with no bounds-checking lets ../../.. walk outside the intended directory | send_file(os.path.join(DIR, input)) | CWE-22 |
| IDOR | Fetching a record by a client-supplied ID with no check that the current user actually owns it | Vault.query.get(item_id) with no ownership filter | CWE-639 |
Resumen — Room de la ruta ofensiva de TryHackMe dedicado al white-box testing: auditar una aplicación cuando ya dispones de su código fuente en lugar de atacarla a ciegas desde fuera. Enseña a leer el código como lo hace un atacante — siguiendo el modelo source-to-sink — y aplica esa metodología a un target Flask real para descubrir y explotar tres vulnerabilidades: SQL Injection, SSTI y Path Traversal.
| Plataforma | TryHackMe |
| Categoría | Offensive — Secure Code Review |
| Dificultad | Medium |
| Room | Web Frameworks: Code Review |
Parte 1 — Metodología
La habilidad central del room no es memorizar funciones peligrosas, sino leer código con intención: no de arriba abajo como una novela, sino preguntando «¿por dónde entra la entrada del usuario y dónde acaba usándose de forma peligrosa?». Cuando respondes eso rápido para un codebase, encontrar bugs deja de ser adivinar y pasa a ser un proceso repetible.
1.1 Cómo leer un codebase desconocido
Abrir ficheros al azar desperdicia horas. El truco es leer en el orden en que fluye una petición real por la app, de modo que cuando llegas al código que maneja peticiones ya entiendes el contexto que lo rodea. Es como registrar un edificio: primero la puerta y el directorio, luego las salas.
| Paso | Qué lees | Por qué importa el orden |
|---|---|---|
| 1 | README / Docs | Dice qué se supone que hace la app, para reconocer cuando algo no encaja con ese propósito. |
| 2 | Manifiesto de dependencias (requirements.txt) | Una única librería desactualizada y fijada puede ser un CVE conocido — merece la pena comprobarlo antes de leer una sola línea de lógica. |
| 3 | Configuración (config.py) | Ajustes como DEBUG = True o una secret key hardcodeada cambian cómo tratar cada bug que encuentres después. |
| 4 | Routing | Es literalmente el mapa de todo lo que el mundo exterior puede tocar. |
| 5 | Auth middleware | Dice qué rutas requieren login y cuáles no — clave para priorizar qué probar primero. |
| 6 | Base de datos / Modelos | Muestra cómo es el acceso a datos «normal», para que cualquier cosa que rompa ese patrón destaque después. |
| 7 | Manejadores de ruta | El análisis en profundidad — a estas alturas ya sabes qué es público, qué está protegido y cómo son las consultas normales. |
En un proyecto Python/Flask en concreto: las dependencias viven en requirements.txt, las rutas se crean con el decorador @app.route, y el flag de debug junto con la secret key viven por convención en config.py.
1.2 El modelo source-to-sink
Es la idea más útil de todo el room. Casi todo bug de inyección se reduce a dos puntos conectados por una comprobación de seguridad rota (o ausente):
- Source — dónde entra el dato no confiable. En Flask:
request.args(query string),request.form(datos de formulario),request.cookiesorequest.json. - Sink — una función donde ese dato, sin sanitizar, causa daño real. Las más habituales:
cursor.execute()con input de cadena en crudo → SQL Injectionsubprocess.run(..., shell=True)→ Command Injectionrender_template_string()sobre input no validado → SSTI
El flujo de trabajo es simple en concepto: empieza en un sink y camina hacia atrás por el código hasta que (a) encuentres un source sin sanitización por medio — bug confirmado — o (b) llegues a un punto donde el dato fue validado o parametrizado correctamente — seguro. En el ejemplo del room, request.args.get("name") fluye directo a render_template_string() sin nada por medio, un SSTI de manual.
1.3 Deja que las herramientas hagan la primera pasada
Leer a mano cada línea de una app grande no escala, así que dos herramientas hacen el trabajo pesado antes de tocar una línea a mano:
grep -rn "patrón" .— búsqueda de texto tonta pero rápida.-rbusca recursivamente en todos los ficheros y-nimprime el número de línea de cada coincidencia, así que obtienes al instante una lista de cada sitio donde aparece una función peligrosa.- Semgrep — más listo que grep porque entiende la estructura del código (vía el AST), no solo el texto. Detecta un patrón peligroso aunque cambien los nombres de variables o el formato. Apuntarlo a un ruleset ya hecho usa el flag
--config:
semgrep --config p/owasp-top-ten .Esto comprueba todo el codebase contra las reglas curadas de OWASP Top 10 en un solo comando. El enfoque práctico: grep primero por velocidad, luego Semgrep por cobertura, y alimenta lo que ambas marquen al trazado manual source-to-sink de 1.2.
1.4 Las clases de vulnerabilidad en juego
| Clase | Qué la causa realmente | Sink típico | CWE |
|---|---|---|---|
| SQL Injection | Construir una query con f-string o + en vez de una consulta parametrizada / con binding | cursor.execute(f"...{input}...") | CWE-89 |
| Command Injection | shell=True entrega toda la cadena a /bin/sh, así que los metacaracteres (;, &&, backticks) se interpretan | subprocess.run(cmd, shell=True) | CWE-78 |
| SSTI | Una cadena influida por el usuario se compila y ejecuta como plantilla, en vez de pasarse como dato inerte a una plantilla estática | render_template_string(input) | CWE-1336 |
| Insecure Deserialization | Reconstruir objetos Python desde un flujo de bytes no confiable deja que el método __reduce__ de un objeto malicioso ejecute código al deserializar | pickle.loads(data) | CWE-502 |
| Path Traversal | Unir input del usuario directo a una ruta del sistema de ficheros sin comprobar límites deja que ../../.. salga del directorio previsto | send_file(os.path.join(DIR, input)) | CWE-22 |
| IDOR | Recuperar un registro por un ID suministrado por el cliente sin comprobar que el usuario actual sea su propietario | Vault.query.get(item_id) sin filtro de propiedad | CWE-639 |
This content is Root Access only. Everything else on the site — the free tier, the whole public library — stays open.
See Root Access plansNo account? Create one free then upgrade from your console.
Este contenido es solo para Root Access. Todo lo demás del sitio — el nivel gratuito, toda la biblioteca pública — sigue abierto.
Ver planes de Root Access¿Sin cuenta? Crea una gratis y luego mejora desde tu consola.