Skip to content

bug: sanitize static asset fullUrl and validate stored file extension - #102

Open
devin-ai-integration[bot] wants to merge 1 commit into
develop-7.0.xfrom
devin/1787182896-asset-fullurl-sanitization
Open

devin-ai-integration[bot] wants to merge 1 commit into
develop-7.0.xfrom
devin/1787182896-asset-fullurl-sanitization

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown

Summary

StaticAssetServiceImpl built an asset's fullUrl straight from client input — entityType/entityId, the optional fileName property and the multipart filename — with only the configurable +,*,% replacement applied, so /, \ and .. survived into StaticAsset.fullUrl and from there into the filesystem write in StaticAssetStorageServiceImpl.createStaticAssetStorage (asset.use.filesystem.storage=true is the shipped default): an admin upload named ../../../x.jsp became an arbitrary file write. The extension check was also bypassable, because it validated the tika-detected content type while the file is stored under the filename's extension.

Sanitization (all in StaticAssetServiceImpl):

stripPathInformation(name)   = FilenameUtils.getName(name without control chars), "" for "."/".."
sanitizePathSegment(seg)     = stripPathInformation, reject if nothing remains   // entityType, entityId
sanitizeFileName(name)       = stripPathInformation, reject if nothing remains   // upload filename
sanitizeAssetPath(path)      = per-segment: drop ""/".", reject ".."             // 'fileName' property
validateAssetURL(url)        = FilenameUtils.normalize(url,true).equals(url) && startsWith("/")

sanitizeFileName is applied in both entry points (createStaticAssetFromFile and createStaticAsset, which is also callable directly), buildAssetURL ends with validateAssetURL, and the storage sink keeps its own validateFileSystemAssetUrl guard plus canonical-path containment so a pre-existing/imported fullUrl cannot escape the file work area either.

Extension validation now runs twice — once on the tika-detected extension and once on the extension the file is actually stored under:

validateExtension(getFileExtension(file), file.getName());
validateExtension(FilenameUtils.getExtension(stripPathInformation(originalFilename)).toLowerCase(), file.getName());

and the shipped disabled.file.extensions default is widened from pdf to the usual server-executable set (jsp, php, asp, sh, exe, war, ...). Behaviour for extension-less uploads under the deny list is unchanged.

Testing

repo1.maven.org and nexus2.broadleafcommerce.org are blocked on this machine, so the module cannot be built here. The new/changed logic was extracted verbatim into standalone harnesses and run against commons-io/commons-lang3: 28 path-traversal payload checks and 13 extension checks (including shell.jsp with PNG content, shell.PhP5, ../../../x.jsp, and allow-list mode) all behave as expected. Unit tests covering both are added in StaticAssetServiceImplTest and StaticAssetStorageServiceImplTest for CI.

Session: https://app.devin.ai/sessions/868bdc1811d742369bb0b366b00501c0

Devin-Org: engineering


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)

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