dromara / dromara/lamp-cloud

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

Open
#412 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
5.8k
Forks
1.8k
PR merge metrics
No merged PRs in 30d

Description

**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")`):

```java
// lamp-base/lamp-base-controller/.../file/controller/FileAnyoneController.java:47,63
@RequestMapping("/anyone/file")
...
@PostMapping(value = "/upload")
public R 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:

```java
// 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:

```java
// 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:

```java
// 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):

```yaml
# 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:

```http
POST /anyone/file/upload HTTP/1.1
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

------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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with FileAnyoneController.java, FileUploadVO.java, AbstractFileStrategy.java, and LocalFileStrategyImpl.java, then review the /anyone/** authorization setting in application.yml. Trace bucket and bizType through the upload path and verify that traversal inputs cannot escape storagePath and that upload access matches the intended authentication policy.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring-boot
Domain
authentication, authorization, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.