Almost every file inclusion vulnerability reduces to one sentence: the application asked the caller which file to open, and the caller answered. The room teaches the three names this failure goes by, and the reason it is worth learning properly is that the severity depends entirely on what happens after the file is chosen.
Reading the wrong file is a disclosure. Loading the wrong file as code is execution. The distance between those two outcomes is the most interesting part of this topic, and it is what separates a room that teaches payloads from a room that teaches a class of bug.
01 · Scope and authorisation
This companion documents the public TryHackMe File Inclusion room, a standard beginner web-security room covering local file inclusion, remote file inclusion and directory traversal. I worked through it in my own session. Everything below stays inside that environment, and I have published no flags, no quiz answers, no credentials and no target addresses. Tool choices and the flags that matter are described so you can run your own test rather than replay a recorded one.
The room maps to CWE-22, improper limitation of a pathname to a restricted directory, CWE-98, improper control of filename for include in a PHP program, and CWE-434, unrestricted upload of a file with a dangerous type, which appears because upload and inclusion combine. The testing approach is documented in the OWASP Web Security Testing Guide under file inclusion and path traversal.
02 · Learning objectives
- Explain why a path built from user input is an instruction rather than a value.
- Use directory traversal reasoning without memorising a payload string.
- Distinguish disclosure from execution and state which one a finding is.
- Explain why denylist filtering fails and what a control has to validate instead.
- Describe the two ingredients that turn a read into execution.
- State what a responder should look for in logs when inclusion is suspected.
03 · Prerequisites and the loop I use
The room assumes basic web knowledge, an intercepting proxy and a request tool. The loop I applied to each task is the one I use for any input-handling bug:
- Observe which parameters influence a file operation, rather than which ones look suspicious.
- Hypothesise the check that is missing or misplaced.
- Choose the least invasive test that would falsify the hypothesis.
- Interpret the response as a property of the system, not a lucky string.
- Update the model and decide whether the finding is disclosure or execution.
The reason observation comes first here is that file inclusion is rarely advertised. The parameter is usually named for its purpose, such as a template, a language, a page or a module, and the vulnerability is that nobody constrained what that name could be.
04 · The model: a path is an instruction
Build this model before touching the lab. An application that takes a filename and opens it is performing two operations: resolving a path, and acting on the result. The developer writes the first with one directory in mind and the second with one purpose in mind. The vulnerability lives in the gap, because the resolver will accept directions the developer never intended to accept.
That produces the two questions worth asking of any file-handling parameter. Where does the resolved path end up, and what does the application then do with what it finds there? The first question decides whether a traversal is possible. The second decides how much it matters.
05 · Directory traversal: the intended directory is a suggestion
Goal. Establish whether the resolved path can leave the directory the developer had in mind.
What to look for. A parameter whose value looks like a filename or a relative path, in a request that returns file content, a rendered template, a download or an error message naming a file. The response body is the evidence: if changing the value changes what comes back, you have located a file operation and can reason about it.
Hypothesis and the least invasive test. The hypothesis is that the value is concatenated into a path without the resolved result being checked. The test is to name a file you know exists outside the intended directory and observe whether the application returns it or refuses. Do this against a benign, well-known system file rather than anything sensitive, and stop at the proof: the point is to establish that the boundary is absent, not to loot the filesystem.
Tool choice and the flags that matter. An intercepting proxy plus a request tool is the right combination, because you are varying one field and comparing responses. The settings worth understanding are the ones that let you see the raw body rather than a rendered view, and that disable automatic redirect following so you can observe the first response. Automation comes later; this stage is a controlled experiment.
Interpretation. Success here is a boundary failure, not yet an impact statement. What you have proved is that the application resolves a path the caller influences without confining it. The severity depends on the second question from the model: what the application does with the file it opened.
Failure mode. The common mistake is assuming a refusal means safety. The refusal may be a filter on the input rather than a check on the destination, which is a different and weaker control, and the next section is about exactly that difference.
Transferable concept. Any parameter that names something on disk is a path. That includes template names, language codes, module identifiers, report names, backup filenames and attachment identifiers. The question is never whether it looks like a path. It is whether the resolved destination is verified.
06 · Local file inclusion: when the file is loaded as code
Goal. Determine whether the parameter controls an include rather than a read, because that changes the finding from disclosure to potential execution.
How to tell. The behaviour differs in a recognisable way. A read returns the file's contents to you. An include executes the file as part of the application, so you receive its output, its errors, or a change in the page's own structure rather than a verbatim copy. Symptomatically, an included script either produces nothing visible or produces output that mixes with the application's, and a non-script file is often echoed in full because there is nothing to execute.
Interpretation. This is the important distinction in the whole room. A traversal that discloses a configuration file is a serious confidentiality finding. An inclusion that accepts a target is a potential code execution finding, because the application has agreed to run whatever it is pointed at. The mechanisms are similar and the consequences are not.
Failure mode. Treating a successful read of an interesting file as the end of the exercise. Interesting files are useful evidence of reach, not an objective. The objective is to understand what class of operation you have found, because that determines what remediation the owner must perform.
Transferable concept. The distinction generalises well beyond this room, and it is the same distinction I drew in the field note on loading a model artefact: reading data is one thing, and having the runtime execute what you supplied is another. Where an application converts a caller's selection into its own execution, the boundary has moved to the caller.
07 · Filter bypass: why blocking a string is not a control
Goal. Understand why an application that filters input can still be traversed, and what the correct control looks like.
The hypothesis. The application has a denylist. It removes or rejects one recognised representation of a parent-directory reference. The hypothesis is that other representations resolve to the same destination and are not covered.
The reasoning, not the payload. A denylist enumerates strings. A filesystem resolves paths. Those two things do not have the same alphabet, so there is always a gap between what a filter anticipates and what a resolver accepts. Which specific representations apply depends on the platform and the language, which is precisely why memorising one is the wrong lesson. What transfers is the direction of the check: the filter inspects how the input looks, while the only reliable check inspects where the resolved path lands.
Interpretation. A bypass proves the filter was the wrong control, not merely that it was incomplete. This matters for remediation, because the owner's instinct is to add another pattern to the denylist. That produces a filter which is bypassed by the next representation, and it is how these bugs survive multiple rounds of patching.
Failure mode. Two. Testing one representation, seeing a refusal, and concluding the parameter is safe. And treating the filter as a boundary when it is a speed bump: the finding should be reported as a missing destination check, with the bypass as supporting evidence rather than as the headline.
Transferable concept. The correct control is an allowlist of accepted values, or an identifier mapped server-side to a known file, followed by resolving the final path and confirming it remains inside the permitted directory. Validate after resolution, because that is the only point at which you know what the filesystem was actually asked to open.
08 · From reading to executing
This is the part people find most interesting, and it is worth teaching as a principle rather than a recipe. Two ingredients are required, and each one is a question about the application.
Ingredient one: an include that accepts a caller-supplied target. That is the capability established in section six. Without it, you have disclosure at most.
Ingredient two: a file whose contents you can influence, which the application will include. This is the piece that surprises people, because the file does not need to be dangerous on its own. Almost any application writes something somewhere: log files, session files, cache files, upload directories, temporary files. If one of those paths accepts content you can shape and the include will load it, the contents become code in the application's process.
The reason this is worth understanding conceptually is that it changes where the defender looks. The remediation is not "sanitise the log file". It is to stop building the include target from user input, and then to reduce what the process can read and execute, so that a bypass has nothing valuable to reach. Defence in depth is not decoration here; it is the difference between a disclosure and an incident.
The remote variant is the same mechanism with the target being a URL rather than a local path. The application fetches and executes code from a host the caller names, which collapses both ingredients into one step. Modern language configurations commonly disable remote fetching by default for exactly this reason, which is why the local path remains the interesting one.
09 · Why the chain works
The room's tasks are variations on one structure, and naming it is what makes the topic portable. A caller supplies a value. The application resolves it into a filesystem path. The application acts on the result without confirming where it landed. Every task is a different answer to "how much does the last step matter", ranging from disclosure to execution.
That structure explains why the fixes are counterintuitive. The instinct is to tighten the input, because the input is what the attacker controls. But the input is unbounded by nature: it is text, and text has more representations than any filter will enumerate. The destination, by contrast, is a single value you can compute and check. This is why the correct control sits after resolution, and why every filter-based fix in this class eventually fails to the next encoding somebody thinks of.
It also explains why the reading-to-execution step is a design problem rather than a bug. Nothing was broken when the log file accepted your text, and nothing was broken when the include opened the file it was told to open. The failure is the composition of two correct behaviours, which is the hardest kind of issue to catch in review and the kind that benefits most from a boundary that does not depend on either one being right.
10 · Defensive controls, logs and detections
| Weakness | Durable control | What to log and alert on |
|---|---|---|
| Traversal (CWE-22) | Never build a path from user input. Map an identifier to a known file server-side, resolve the final path, and confirm the canonical result stays inside the permitted directory. | Requests whose file parameter contains path separators or parent references, and any resolved path outside the application directory. |
| Inclusion (CWE-98) | Include only from a fixed set. Never allow a remote target. Keep application code, data and writable directories separate so that content the application writes cannot be executed by the application. | Include or require calls whose target varies by request, and template or module parameters accepting values outside the known set. |
| Upload combined with inclusion (CWE-434) | Store uploads outside the executable path, generate server-side filenames, validate by content rather than by extension, and serve uploads through a handler that sets a non-executable content type. | Files written to web-served directories, content-type mismatches between header and content, and uploads followed by a request to the same path. |
| Blast radius | Run the web process under a least-privilege account with read access only where it needs it, and separate tenants and applications so a bypass reaches one boundary rather than the whole host. | Reads of files outside the application directory by the web process, and unexpected access to credential, key or configuration paths. |
The last row is the one I would prioritise in a review, because it is the only control that reduces the consequence of the others failing. A process that cannot read a credential store turns a traversal into a much smaller finding, and a process that cannot execute in its own writable directories turns a composition bug back into a disclosure.
11 · Misconceptions and troubleshooting
- "Traversal and LFI are the same bug." They share a cause and differ in consequence. Traversal reads. Inclusion executes. Reporting one as the other either exaggerates or understates the finding.
- "The parameter refused my test, so it is filtered and safe." A refusal may be an input filter rather than a destination check. Those are different controls with different failure modes, and the second is what you need.
- "There is no upload feature, so inclusion is not exploitable." The application already writes files for its own purposes. Any of those paths can be the second ingredient, which is why the finding should not depend on a file upload existing.
- "Restricting the extension fixes it." Extension checks are input-shape checks. They are worth having and they are not a boundary, for the same reason denylists are not.
Troubleshooting the reasoning comes down to four questions asked of any file-handling parameter. Where does the value come from? Where does the resolved path land? What does the application do with what it finds? And what could the process reach if the answer to the second question were somewhere unexpected? Answering the last one honestly is usually what turns a low finding into an urgent one, or shows that it is genuinely contained.
12 · Memory aids and practice checklist
Memory aid: a path is an instruction. You are not telling the application what to read. You are telling it what to do, and the only question is whether anybody checked where the instruction pointed.
- Find every parameter that names a file, template, module, language, page or attachment.
- For each, ask whether the resolved destination is checked, as opposed to the input's appearance.
- Establish disclosure or execution before writing up, and state which one it is.
- Look for files the application writes that you can influence; they are the second ingredient.
- Confirm the resolved path is canonicalised before it is compared to the permitted directory.
- Confirm the web process cannot read credentials or execute in its writable directories.
- Confirm uploads are stored out of the executable path and served with a non-executable type.
13 · What the room teaches
The room's lesson, stated once: when an application lets a caller choose what it opens, the caller has taken part of the application's decision-making. Everything else in the topic, including the three names and the escalation from reading to executing, follows from that one sentence.
The modern company for that idea is worth reading next. The field note on a model file being a program applies the same reasoning to a model loader, and the earlier note on a second session path that skipped the first path's checks is the identity version of the same discipline. If you want the retrieval equivalent, the RAG retrieval companion covers what happens when a system chooses content by relevance alone.
Frequently asked questions
What is the difference between directory traversal and file inclusion?
Traversal is escaping an intended directory by manipulating the path. Inclusion is the application loading a file as part of its own execution. Traversal reads files, while inclusion can execute them, which is why inclusion is the more severe of the two and why the distinction changes the remediation.
Why do filters that block parent directory sequences fail?
Because a denylist enumerates strings while a filesystem resolves encodings and normalisations of the same destination. There is always a representation the filter did not anticipate. The control has to validate the resolved path against an allowlist rather than inspect how the input appears.
What is remote file inclusion?
It is inclusion where the target can be a remote URL, so the application fetches and executes code from a host the attacker names. It collapses the two ingredients of inclusion-to-execution into a single step, and modern language configurations commonly disable remote fetching by default for that reason.
How do you stop file inclusion vulnerabilities?
Never build a path from user input. Map identifiers to known files through an allowlist, resolve the final path and verify it stays inside the permitted directory, keep uploads and writable directories outside the executable path, and restrict what the process can read so a bypass has nothing valuable to reach.