SpikerSoft.EventHandlers.BlogMediaProcessor/Services/BlogMediaMoveConsumer.cs builds the destination path for uploaded blog media directly from event fields:
evt.Username (and, to a lesser extent, FileExtension) originate from upload-time input. They are not run through any containment check, so a value containing ../ — or an absolute path, which Path.Combine silently lets replace the base — could write/create directories outside/app/blogs, and the same evt.Username is reflected into finalUrl = /blog-pictures/{Username}/{file}.
Exploitability depends on what the Keycloak realm permits in a username, but relying on that is fragile; the sink should be hardened regardless.
For contrast, the ebook mover (FileMovementService) is already safe — ISBNs are validated by BookUploadValidator (regex + checksum) and the filename is Path.GetFileName-stripped — so no change was needed there. The Ops query builders similarly demonstrate the escape-at-the-sink discipline.
Fix
Added SpikerSoft.Common.IO.PathSafety.CombineContained(baseDir, ...segments) — resolves the combined path and throws UnauthorizedAccessException if it escapes the base (catches ../ traversal and absolute-path replacement) while leaving normal names/sub-paths untouched. Routed both blog-media combines through it.
Verification
PathSafetyTests (8): ../, multi-level ../../, embedded a/../../b, and absolute-path segments all throw; normal single/multi-segment names stay within base. Build clean.
Resolved by spikersoft-backend PR (linked below). Filing for traceability.
## Observation
`SpikerSoft.EventHandlers.BlogMediaProcessor/Services/BlogMediaMoveConsumer.cs` builds the destination path for uploaded blog media directly from event fields:
```csharp
var userDir = Path.Combine(finalBasePath, evt.Username); // finalBasePath = /app/blogs
var finalPath = Path.Combine(userDir, $"{evt.MediaId}{evt.FileExtension}");
```
`evt.Username` (and, to a lesser extent, `FileExtension`) originate from upload-time input. They are **not** run through any containment check, so a value containing `../` — or an absolute path, which `Path.Combine` silently lets *replace* the base — could write/create directories **outside** `/app/blogs`, and the same `evt.Username` is reflected into `finalUrl = /blog-pictures/{Username}/{file}`.
Exploitability depends on what the Keycloak realm permits in a username, but relying on that is fragile; the sink should be hardened regardless.
For contrast, the ebook mover (`FileMovementService`) is **already safe** — ISBNs are validated by `BookUploadValidator` (regex + checksum) and the filename is `Path.GetFileName`-stripped — so no change was needed there. The Ops query builders similarly demonstrate the escape-at-the-sink discipline.
## Fix
Added `SpikerSoft.Common.IO.PathSafety.CombineContained(baseDir, ...segments)` — resolves the combined path and throws `UnauthorizedAccessException` if it escapes the base (catches `../` traversal and absolute-path replacement) while leaving normal names/sub-paths untouched. Routed both blog-media combines through it.
## Verification
`PathSafetyTests` (8): `../`, multi-level `../../`, embedded `a/../../b`, and absolute-path segments all throw; normal single/multi-segment names stay within base. Build clean.
Resolved by spikersoft-backend PR (linked below). Filing for traceability.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Observation
SpikerSoft.EventHandlers.BlogMediaProcessor/Services/BlogMediaMoveConsumer.csbuilds the destination path for uploaded blog media directly from event fields:evt.Username(and, to a lesser extent,FileExtension) originate from upload-time input. They are not run through any containment check, so a value containing../— or an absolute path, whichPath.Combinesilently lets replace the base — could write/create directories outside/app/blogs, and the sameevt.Usernameis reflected intofinalUrl = /blog-pictures/{Username}/{file}.Exploitability depends on what the Keycloak realm permits in a username, but relying on that is fragile; the sink should be hardened regardless.
For contrast, the ebook mover (
FileMovementService) is already safe — ISBNs are validated byBookUploadValidator(regex + checksum) and the filename isPath.GetFileName-stripped — so no change was needed there. The Ops query builders similarly demonstrate the escape-at-the-sink discipline.Fix
Added
SpikerSoft.Common.IO.PathSafety.CombineContained(baseDir, ...segments)— resolves the combined path and throwsUnauthorizedAccessExceptionif it escapes the base (catches../traversal and absolute-path replacement) while leaving normal names/sub-paths untouched. Routed both blog-media combines through it.Verification
PathSafetyTests(8):../, multi-level../../, embeddeda/../../b, and absolute-path segments all throw; normal single/multi-segment names stay within base. Build clean.Resolved by spikersoft-backend PR (linked below). Filing for traceability.
Resolved in spikersoft-backend PR #127 (merged to
masteras7364964).PathSafety.CombineContainednow contains the blog-media move to/app/blogs(rejects../traversal + absolute-path replacement);FileMovementServiceconfirmed already safe. 8/8 tests. Closing.