Skip to content

Strip path traversal from uploaded asset file names - #93

Open
devin-ai-integration[bot] wants to merge 1 commit into
develop-7.0.xfrom
devin/1786136740-asset-upload-path-traversal
Open

devin-ai-integration[bot] wants to merge 1 commit into
develop-7.0.xfrom
devin/1786136740-asset-upload-path-traversal

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 7, 2026

Copy link
Copy Markdown

A Brief Overview

An admin asset upload derived the persisted StaticAsset.fullUrl straight from the client-supplied multipart filename. The only normalization was fileName.replaceAll("[" + notAllowedCharsInFileName + "]", replacementString) (default +,*,%), so /, \ and .. survived. buildAssetURL then concatenated the also client-derived entityType/entityId and that filename, and StaticAssetStorageServiceImpl.createStaticAssetStorage used the result verbatim to build a filesystem path (asset.use.filesystem.storage=true is the shipped default):

multipart filename "../../../../webapps/ROOT/shell.jsp"
  -> fullUrl "/product/100/../../../../webapps/ROOT/shell.jsp"
  -> FilenameUtils.normalize(workArea + fullUrl) = a path outside the work area -> file written there

StaticAssetServiceImpl now builds asset urls out of sanitized segments instead of relying on the configurable invalid-char list:

  • stripPathInformation(name) = strip control characters, then FilenameUtils.getName(...) (handles both separator styles), then reject a bare ./... Applied to the uploaded filename in normalizeFileExtension/createStaticAssetFromFile and again in createStaticAsset, so the direct createStaticAsset(InputStream, fileName, ...) API is covered too.
  • entityType/entityId go through sanitizePathSegment (single segment, no separators).
  • The optional fileName property is documented as a url, so it keeps multiple segments via sanitizeAssetPath: empty/. segments dropped, .. rejected.
  • validateAssetURL is a final assertion that the assembled url is a normalized absolute path.

StaticAssetStorageServiceImpl.createStaticAssetStorage additionally rejects any asset url that is not already normalized before it is turned into a filesystem path, so the write sink is safe even for asset records created elsewhere.

Anything that reduces to nothing (../.., ., /) is rejected with an IllegalArgumentException rather than silently renamed. Ordinary uploads are unchanged: img.png with entityType=product, entityId=100 still yields /product/100/img.png.

Additional context

Unit tests added to StaticAssetServiceImplTest (traversal, Windows separators, absolute paths, NUL byte, traversal-only names, attacker-controlled entity segments, the fileName property) and a new StaticAssetStorageServiceImplTest for the sink guard.

Maven Central and the Broadleaf nexus are not reachable from the environment this was written in, so the module could not be built; the changed files were syntax-checked with javac and the new logic was additionally executed standalone against commons-io 2.22/2.13 + commons-lang3 with the same payloads the unit tests use (all pass).

Add Labels to the right panel: Security, Bug, critical, ready-for-code-review


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)

Uploaded asset file names were only run through the configurable
static.asset.invalid.chars.in.filename replacement (+,*,%), so directory
separators and .. segments survived into the asset fullUrl and, from
there, into the filesystem storage write path.

Asset urls are now assembled from sanitized segments only, and the
filesystem writer rejects any asset url that is not already normalized.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants