Skip to content

Arbitrary file write via anonymous-path file upload (path traversal in bucket/bizType) #412

Description

@Ku4D3

Severity: High · CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)

Summary

The file-upload controller mapped under the /anyone/ prefix joins request-controlled bucket and bizType values directly into the local storage path without any traversal filtering. Although the per-file name is a server-generated UUID, the directory components are attacker-controlled, so a caller can supply ../ sequences in bucket or bizType to escape the configured storage root and write uploaded content to an arbitrary writable location. The /anyone/** space is configured to skip URI-level authorization, so no privileged role is required; if the gateway's login-token check is not enforced for this path, the endpoint is fully unauthenticated.

Vulnerability chain

Stage Component Location
Source POST /anyone/file/upload, params bizType / bucket FileAnyoneController.java:65
Transform relative path = getPath(bizType, uniqueFileName); final = Paths.get(storagePath, bucket, path) AbstractFileStrategy.java:97, LocalFileStrategyImpl.java:46
Sink new File(absolutePath) + FileUtils.copyInputStreamToFile(...) LocalFileStrategyImpl.java:49-50

The bucket request parameter and the bizType field both flow into the assembled absolute path. bizType becomes a path segment via getPath (joined with /), and bucket is concatenated by Paths.get. Because neither is validated, ../ in either value escapes the storagePath root. The stored filename itself is a UUID (getUniqueFileName), which does not help — the directory is fully attacker-directed.

Key code

The controller is mapped under the no-URI-authorization /anyone space (@RequestMapping("/anyone/file")):

// lamp-base/lamp-base-controller/.../file/controller/FileAnyoneController.java:47,63
@RequestMapping("/anyone/file")
...
@PostMapping(value = "/upload")
public R<FileResultVO> upload(@RequestParam(value = "file") MultipartFile file,
                              @Validated FileUploadVO fileUploadVO) {
    return R.success(fileService.upload(file, fileUploadVO));
}

bizType and bucket are user-supplied and only @NotBlank-validated:

// lamp-base/lamp-base-entity/.../file/vo/param/FileUploadVO.java
@NotBlank(message = "请填写业务类型")
private String bizType;
private String bucket;

Both values are concatenated into the filesystem path with no canonicalization:

// lamp-base/lamp-base-biz/.../file/strategy/impl/local/LocalFileStrategyImpl.java:42-50
String uniqueFileName = getUniqueFileName(file);
String path = getPath(file.getBizType(), uniqueFileName);
String absolutePath = Paths.get(local.getStoragePath(), bucket, path).toString();   // :46 — traversal in bucket/path

java.io.File outFile = new java.io.File(absolutePath);
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), outFile);            // :50

getPath places bizType directly as a path segment:

// AbstractFileStrategy.java:97
protected String getPath(String bizType, String uniqueFileName) {
    return new StringJoiner(StrPool.SLASH)
            .add(bizType).add(getDateFolder()).add(uniqueFileName).toString();
}

/anyone/** is in the skip-authorization category (URI-permission checks bypassed for matching paths):

# lamp-support/lamp-boot-server/src/main/resources/application.yml:73-75
anyone: # 请求中 需要携带Tenant 且 需要携带Token(不需要登录),但不需要验证uri权限
  ALL:
    - /anyone/**

Proof of Concept

Request-level only. No URI-level authorization is required. With bucket (or bizType) containing ../../, the uploaded bytes land outside the storage root:

POST /anyone/file/upload HTTP/1.1
Host: <lamp-host>
Content-Type: multipart/form-data; boundary=----boundary

------boundary
Content-Disposition: form-data; name="bizType"

any
------boundary
Content-Disposition: form-data; name="bucket"

../../../../opt/app/config
------boundary
Content-Disposition: form-data; name="file"; filename="note.txt"
Content-Type: application/octet-stream

<uploaded file contents>
------boundary--

Impact

An attacker who can reach the /anyone/file/upload endpoint (no privileged role needed; unauthenticated if the gateway token check is not enforced for this path) can write attacker-controlled content to arbitrary writable paths on the host, escaping the intended storage directory. Overwriting configuration or classpath resources can lead to remote code execution. The traversal component (bucket/bizType) is fully attacker-controlled; only the leaf filename is a UUID.

Remediation

  • Canonicalize the final path (Path.normalize() / getCanonicalFile()) and reject any result outside the configured storagePath.
  • Whitelist bucket and bizType against an explicit allow-list, or reject values containing path separators, .., or absolute paths at the controller boundary.
  • Require authentication and an explicit role for the upload endpoint; do not expose a file-write endpoint under the no-authorization /anyone/** prefix.
  • Derive the storage relative path from trusted server state rather than echoing request values into the filesystem path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions