Web Frameworks: Code Review | TryHackMe
Read web app source like an attacker: trace user input to dangerous sinks, triage with Semgrep.
Learning Objectives
- Distinguish black-box, grey-box, and white-box testing, and recognise when source access changes the approach
- Orient quickly in an unfamiliar codebase using a fixed reading order
- Map the attack surface from routing and configuration without running the application
- Trace user-controlled input from source to sink in Python web code
- Triage a codebase with grep, ripgrep, and Semgrep
- Recognise SQL injection, command injection, Server-Side Template Injection (SSTI), insecure deserialisation, path traversal, broken access control, and hardcoded secrets in source
- Apply the full method to a purpose-built vulnerable Flask application
Prerequisites
- OWASP Top 10 (2025): The web bug classes we will learn to spot in source
- Burp Suite: The Basics: Black-box web testing, the counterpart to the white-box approach here
- SQL Injection: One of the sink types we will trace from source to a working exploit
- Content Discovery: Finding the files and endpoints that source review then explains
Answer the questions below
- At step 2 of the reading order we check the dependency manifest, because a pinned old version is a lead we can search against a CVE database. Which file conventionally holds that manifest in a Python project?
requirements.txt
- Which decorator marks a function as a route, and therefore an entry point, in a Flask application?
@app.route
- We read configuration before any route handler because a setting like
DEBUG = Truechanges how every later bug behaves. In a Flask app, which file conventionally holds that debug flag and the secret key?
config.py
- Tracing backwards from a
cursor.executecall, we walk the value back to the point where it entered the application from the client. What do we call that entry point?
source
render_template_stringandsubprocess.run(..., shell=True)are both examples of which type of location in our source-to-sink model?
sink
- In the worked example,
request.args.get("name")flows intorender_template_stringwith no validation. Which vulnerability class does that source-to-sink path create?
SSTI
- Our first triage pass is a single
grepthat searches every Python file recursively and prints the line number of each hit so we can jump straight to it. Which two-flag combination gives us recursion plus line numbers?
-rn
- Semgrep matches code structure rather than text, and we point it at a community ruleset such as
p/owasp-top-ten. Which command-line flag selects that ruleset?
- -config
- With
subprocess.run(), the difference between safe and exploitable is whether the input is parsed by/bin/sh. Which keyword argument, when present, hands the whole command string to the shell and so opens command injection? Give it as written in code, including its value.
shell=True
- Insecure deserialisation reaches code execution because the deserialiser can be told to call arbitrary objects through
__reduce__. Which Python function in the task is the worst offender, loading attacker bytes back into objects?
pickle.loads
- We read
send_file(os.path.join(UPLOAD_DIR, filename))on user input as the dangerous shape for path traversal. Which Flask function is its safe counterpart, routing the path throughsafe_joinand returning a 404 when it escapes the directory?
send_from_directory
- Reading a handler, we find
Vault.query.get(item_id)keyed on the URL id with noowner_id = current_usercondition in the filter. What is the common acronym for the access-control flaw this creates?
IDOR
- The search query filters on the logged-in user and selects two columns, so reach the
system_flagstable with aUNIONof a matching column count. Exploit the SQL injection to extract the stored flag. What is the flag?
THM{un10n_b4s3d_sql1_dump3d}
- The same handler echoes the query back through
render_template_string. Climb from{{7*7}}to command execution and read theFLAG2value from the application's environment. What is the flag?
THM{j1nj4_ss71_to_rc3}
- The download handler joins our filename onto the upload directory with no validation. Walk out of that directory to read
/flag3.txt. What is the flag?
THM{send_file_tr4v3rs4l_w1n}
Get Ryca’s stories in your inbox
Join Medium for free to get updates from this writer.
CONCLUSION
White-box testing is a reading problem before it is an exploitation problem. The exploits at the end of this room were short, because the work was in the reading that came first. The method holds together as five steps we now own: orient in the codebase, map the attack surface from routing and configuration, trace user input from source to sink, triage with grep and Semgrep, then verify each candidate by hand.
What we exercised here repeats on real targets:
- The first ten minutes are reading order, not bug hunting. Manifest, config, routes, auth, then handlers.
- A source reaching a sink with no safe step between them is the shape of almost every injection bug.
grepand Semgrep produce candidates, never confirmed findings. Manual review is the confirmation step.- The safe and dangerous counterparts (
render_templateagainstrender_template_string,send_from_directoryagainstsend_file, placeholders against f-strings) are what let us read a bug on sight. - Access control and hardcoded secrets are found by noticing what is missing, not by tracing data.
The language and framework only change the names: the sink is a raw query whether it is built with an f-string here, with Spring’s JdbcTemplate in Java, or with string concatenation in C#; the template engine differs but server-side template injection reads the same. We learned a way to read code for vulnerabilities, not a Python trick, so carry the method to whatever stack the next engagement hands us. If we want to drill into a single bug class, TryHackMe has dedicated rooms for SQL injection, SSTI, and the remaining OWASP Top 10 categories. On a real engagement with source in hand, start every review the same way: read the config, list the routes, and follow the input.









