Insecure file-serving pattern: `GetItemPictureById` allows path traversal / arbitrary file read via `PictureFileName'
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 68/100
Research direction
Start in src/Catalog.API/Apis/CatalogApi.cs at GetItemPictureById and GetFullPath, then trace PictureFileName through CreateItem and UpdateItem. Exercise the picture endpoint with rooted and ..-containing names, and confirm that valid images still serve while resolved paths cannot leave the Pics directory.
Written by the indexing model from the issue text.
Description
Summary
Catalog.API's product-image endpoint builds a filesystem path by concatenating a database-stored, client-controllable PictureFileName directly into Path.Combine(..., "Pics", pictureFileName) and streams the result with TypedResults.PhysicalFile, with no containment check. Because Path.Combine lets a rooted or ..-containing final segment escape the Pics directory, a caller can make the endpoint return arbitrary files from the host filesystem (e.g. appsettings.json, project source, /etc/passwd).
Since this is a reference application that many developers copy patterns from, the file-serving idiom here teaches an insecure practice (CWE-22). The fix is small and self-contained.
Affected code
src/Catalog.API/Apis/CatalogApi.cs
// GetItemPictureById (~line 205) — PictureFileName comes from the stored CatalogItem
var path = GetFullPath(environment.ContentRootPath, item.PictureFileName);
...
return TypedResults.PhysicalFile(path, mimetype, lastModified: lastModified);
// ~line 420
public static string GetFullPath(string contentRootPath, string pictureFileName) =>
Path.Combine(contentRootPath, "Pics", pictureFileName); // <-- no normalization / containment
PictureFileName is bound straight from the request body in CreateItem/UpdateItem (product.PictureFileName), so it is fully attacker-controlled.
Steps to reproduce
Observed scope: any real on-disk file the process can read is returned. (/proc/* pseudo-files are not returned — PhysicalFile reports Content-Length: 0 for procfs and Kestrel aborts the response — so process environment variables are not exfiltrable through this particular sink.)
Impact
Unrestricted local file read of the Catalog.API host: appsettings*.json, mounted secret files, K8s service-account tokens (/var/run/secrets/...), data-protection keys, application source, etc. In the default topology Catalog.API is an internal service, so realistic exposure requires it to be network-reachable (direct exposure, misconfiguration, or via SSRF/lateral movement) — but the file-read defect itself is unconditional.
Suggested fix
Constrain the filename to the Pics directory. Either reduce it to a bare filename, or verify the resolved path stays inside the base:
public static string GetFullPath(string contentRootPath, string pictureFileName)
{
// Reject any directory component supplied by the caller.
var safeName = Path.GetFileName(pictureFileName);
var picsRoot = Path.Combine(contentRootPath, "Pics");
var full = Path.GetFullPath(Path.Combine(picsRoot, safeName));
// Defense in depth: ensure we never escape the Pics directory.
if (!full.StartsWith(picsRoot + Path.DirectorySeparatorChar, StringComparison.Ordinal))
throw new InvalidOperationException("Invalid picture path.");
return full;
}
Optionally also validate PictureFileName on write (CreateItem/UpdateItem) to reject path separators and ...
Notes
Related hardening for the same endpoints (lower priority, may be intentional for the sample): the Catalog.API write endpoints (POST/PUT/DELETE /api/catalog/items) have no authorization, which is what lets the malicious PictureFileName be stored. If the reference app intends these to be callable only via an authenticated path, adding RequireAuthorization on the write routes would also break the chain. I'm happy to open a PR with the fix above if it's welcome.
- Dominant language
- C#
- Stars
- 10.9k
- Forks
- 3.8k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 4
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/eShop
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
-
Difficulty 4/5 3-5 days Newbie friendliness 38/100
-
Difficulty 3/5 1-2 days Newbie friendliness 30/100
Similar issues
-
type/automation type/tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
t/bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
ci-failure-cause test-failure
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
area:auth FE mvp P3
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
klasolsson81/jobbliggaren#1788 ·