CWE-23 is the "dot-dot" half of path traversal. MITRE defines it as a product using external input to build a pathname that should be inside a restricted directory, without properly neutralizing sequences such as ".." that can resolve to a location outside it.
It sits under the broader CWE-22. Where CWE-22 covers every escape from a restricted folder, CWE-23 is specifically the relative kind: input that climbs up using parent directory references. The absolute kind, where the input is a full path, is tracked separately.
The record lists web based systems, AI and ML products and technology neutral software among the platforms, so the weakness is not tied to a language.
How it happens
A relative path says where to go starting from the current location. ".." means the parent folder. A string that includes enough of them walks up through the tree, then back down into any directory the process can reach.
The flow is almost always the same:
- A request carries a name, such as a file, a package, a dataset, a layer or an attachment.
- The code joins that name to a base directory.
- The code opens, reads, writes or deletes the result.
- Nobody checked where the result actually points.
Where the name comes from varies more than people expect. The observed cases in the record include a header field, a digest value from a remote registry and a URL path:
- CVE-2024-37032: an LLM management tool did not validate the format of a digest value from a private, untrusted model registry, enabling relative path traversal.
- CVE-2024-0520: a dataset management product allowed relative and absolute path traversal to overwrite files through the Content-Disposition header.
- CVE-2019-20916: a Python package manager did not correctly restrict the filename in a Content-Disposition header, allowing arbitrary file reads with sequences such as "../".
- CVE-2022-45918: a chain in a learning management tool where a debugger used external input to find earlier session logs and did not validate the path.
Three of those four involve a value that came from a remote server or an upload header, not from a person typing in a browser. The attacker may be the other end of a download. That changes the threat model for tools that fetch things, such as package managers and model loaders.
A compact vulnerable example in Go:
func serve(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("file")
data, err := os.ReadFile("/srv/docs/" + name) // name may hold ../
if err != nil {
http.NotFound(w, r)
return
}
w.Write(data)
}
Concatenation is no safer than join. Sending "../../etc/hostname" reads outside the folder.
What an attacker gains
MITRE lists four outcomes.
- Execute unauthorized code or commands: creating or overwriting files that run, such as programs and libraries.
- Modify files or directories: overwriting or creating important files. If one backs a security mechanism, the mechanism can be bypassed. The example given is appending a new account to a password file.
- Read files or directories: exposing sensitive data by traversing the file system, or reading a password file to learn credentials.
- Denial of service: overwriting, deleting or corrupting critical files so the product cannot work.
The cases above show the first two in practice. Overwriting files through an upload header is a write, and writes to the right place become code execution.
How to find it
The record names automated static analysis, which models data flow from external input to file operations. Static analysis is a good first pass, because the pattern is regular.
Add targeted manual checks:
- Look at every place a remote value becomes part of a path. Headers, digests, manifest entries and archive member names are common.
- Check download and extraction code in your own tooling, not only your web handlers.
- In tests, send ".." in single, repeated and encoded forms, and make sure the request is refused rather than merely returning not found.
How to fix it
The record has no mitigation list, so these steps come from the nature of the flaw.
- Do not build paths from untrusted strings when an ID will do.
- Canonicalize, then confirm containment. Resolve the full path, then check that it is inside the allowed base, comparing whole components.
- Validate remote values by format before using them. A digest should look like a digest, with a fixed length and character set. A value that does not match is rejected before it gets near a path.
- Strip directory parts from names that should be bare file names, and reject names with separators.
- For archives and downloads, check each destination path before writing.
- Run with least privilege, and keep the write location separate from anything executable.
A Go fix:
func serve(w http.ResponseWriter, r *http.Request) {
base := "/srv/docs"
name := r.URL.Query().Get("file")
full := filepath.Join(base, filepath.Clean("/"+name))
rel, err := filepath.Rel(base, full)
if err != nil || strings.HasPrefix(rel, "..") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
data, err := os.ReadFile(full)
if err != nil {
http.NotFound(w, r)
return
}
w.Write(data)
}
Cleaning the name with a leading slash first removes any climbing above the root of that string. The relative path check then confirms the result stays under base.
Several of the record's cases are flaws in packages others depend on. Keeping dependencies current and knowing which ones carry traversal issues belongs in the same fix list. The free in-browser dependency scan at gold.safeguard.sh/scan needs no login, keeps your file on your machine and covers up to 100 dependencies.
Quick check
How is CWE-23 different from CWE-22?
CWE-22 is the general weakness. CWE-23 is the relative form, built on ".." sequences. The absolute form, where the input is a complete path, is covered separately.
Can the attacker be a remote server rather than a user?
Yes. Several observed cases involve values from a registry or download headers. Tools that fetch content need to validate what the other side sends.