Skip to content

Commit e9b6b8b

Browse files
committed
fix(security): mask sensitive fields and block upload path traversal
1 parent 4097198 commit e9b6b8b

4 files changed

Lines changed: 162 additions & 3 deletions

File tree

data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/controller/FileUploadController.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.io.IOException;
2323
import java.nio.file.Files;
2424
import java.nio.file.Path;
25-
import java.nio.file.Paths;
2625
import lombok.AllArgsConstructor;
2726
import lombok.extern.slf4j.Slf4j;
2827
import org.springframework.http.MediaType;
@@ -91,9 +90,20 @@ public ResponseEntity<byte[]> getFile(ServerHttpRequest request) {
9190
String requestMapPath = this.getClass().getAnnotation(RequestMapping.class).value()[0];
9291
String requestPath = request.getPath().value();
9392
String urlPrefix = fileStorageProperties.getUrlPrefix();
94-
String filePath = requestPath.substring(requestMapPath.length() + urlPrefix.length());
93+
String requestPrefix = requestMapPath + urlPrefix + "/";
94+
if (!requestPath.startsWith(requestPrefix)) {
95+
return ResponseEntity.badRequest().build();
96+
}
97+
String filePath = requestPath.substring(requestPrefix.length());
98+
if (filePath.isBlank()) {
99+
return ResponseEntity.badRequest().build();
100+
}
95101

96-
Path fullPath = Paths.get(fileStorageProperties.getPath(), filePath);
102+
Path basePath = fileStorageProperties.getLocalBasePath().toAbsolutePath().normalize();
103+
Path fullPath = basePath.resolve(filePath).normalize();
104+
if (!fullPath.startsWith(basePath)) {
105+
return ResponseEntity.status(403).build();
106+
}
97107

98108
if (!Files.exists(fullPath) || Files.isDirectory(fullPath)) {
99109
return ResponseEntity.notFound().build();

data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/entity/Agent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.alibaba.cloud.ai.dataagent.entity;
1717

1818
import com.fasterxml.jackson.annotation.JsonFormat;
19+
import com.fasterxml.jackson.annotation.JsonIgnore;
1920
import lombok.AllArgsConstructor;
2021
import lombok.Builder;
2122
import lombok.Data;
@@ -44,6 +45,7 @@ public class Agent {
4445
private String status; // Status: draft-pending publication, published-published,
4546
// offline-offline
4647

48+
@JsonIgnore
4749
private String apiKey; // API Key for external access, format sk-xxx
4850

4951
@Builder.Default

data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/entity/Datasource.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.alibaba.cloud.ai.dataagent.entity;
1717

1818
import com.fasterxml.jackson.annotation.JsonFormat;
19+
import com.fasterxml.jackson.annotation.JsonIgnore;
1920
import lombok.Builder;
2021
import org.springframework.format.annotation.DateTimeFormat;
2122

@@ -44,8 +45,10 @@ public class Datasource {
4445

4546
private String username;
4647

48+
@JsonIgnore
4749
private String password;
4850

51+
@JsonIgnore
4952
private String connectionUrl;
5053

5154
private String status;
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.cloud.ai.dataagent.controller;
17+
18+
import com.alibaba.cloud.ai.dataagent.entity.Agent;
19+
import com.alibaba.cloud.ai.dataagent.entity.Datasource;
20+
import com.alibaba.cloud.ai.dataagent.properties.FileStorageProperties;
21+
import com.alibaba.cloud.ai.dataagent.service.file.FileStorageService;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.io.TempDir;
25+
import org.springframework.core.io.Resource;
26+
import org.springframework.http.ResponseEntity;
27+
import org.springframework.http.codec.multipart.FilePart;
28+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
29+
import org.springframework.web.multipart.MultipartFile;
30+
import reactor.core.publisher.Mono;
31+
32+
import java.nio.charset.StandardCharsets;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
35+
36+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
37+
import static org.junit.jupiter.api.Assertions.assertEquals;
38+
import static org.junit.jupiter.api.Assertions.assertFalse;
39+
import static org.junit.jupiter.api.Assertions.assertTrue;
40+
41+
class FileAndSerializationSecurityTest {
42+
43+
private final ObjectMapper objectMapper = new ObjectMapper();
44+
45+
@TempDir
46+
Path tempDir;
47+
48+
@Test
49+
void agentSerializationShouldHideApiKey() throws Exception {
50+
Agent agent = Agent.builder().id(1L).name("demo").apiKey("sk-test-secret").apiKeyEnabled(1).build();
51+
52+
String json = objectMapper.writeValueAsString(agent);
53+
54+
assertFalse(json.contains("\"apiKey\":"));
55+
assertFalse(json.contains("sk-test-secret"));
56+
assertTrue(json.contains("apiKeyEnabled"));
57+
}
58+
59+
@Test
60+
void datasourceSerializationShouldHideCredentials() throws Exception {
61+
Datasource datasource = Datasource.builder()
62+
.id(1)
63+
.name("mysql")
64+
.username("root")
65+
.password("secret")
66+
.connectionUrl("jdbc:mysql://127.0.0.1:3306/test")
67+
.build();
68+
69+
String json = objectMapper.writeValueAsString(datasource);
70+
71+
assertTrue(json.contains("username"));
72+
assertFalse(json.contains("\"password\":"));
73+
assertFalse(json.contains("\"connectionUrl\":"));
74+
assertFalse(json.contains("jdbc:mysql://127.0.0.1:3306/test"));
75+
}
76+
77+
@Test
78+
void getFileShouldReturnFileWithinBasePath() throws Exception {
79+
Path file = tempDir.resolve("avatars").resolve("ok.txt");
80+
Files.createDirectories(file.getParent());
81+
byte[] expected = "safe-content".getBytes(StandardCharsets.UTF_8);
82+
Files.write(file, expected);
83+
84+
FileUploadController controller = new FileUploadController(buildFileStorageProperties(),
85+
new NoopFileStorageService());
86+
MockServerHttpRequest request = MockServerHttpRequest.get("/api/upload/uploads/avatars/ok.txt").build();
87+
88+
ResponseEntity<byte[]> response = controller.getFile(request);
89+
90+
assertEquals(200, response.getStatusCode().value());
91+
assertArrayEquals(expected, response.getBody());
92+
}
93+
94+
@Test
95+
void getFileShouldBlockPathTraversal() throws Exception {
96+
Path parentSecret = tempDir.getParent().resolve("secret.txt");
97+
Files.write(parentSecret, "top-secret".getBytes(StandardCharsets.UTF_8));
98+
99+
FileUploadController controller = new FileUploadController(buildFileStorageProperties(),
100+
new NoopFileStorageService());
101+
MockServerHttpRequest request = MockServerHttpRequest.get("/api/upload/uploads/../secret.txt").build();
102+
103+
ResponseEntity<byte[]> response = controller.getFile(request);
104+
105+
assertEquals(403, response.getStatusCode().value());
106+
}
107+
108+
private FileStorageProperties buildFileStorageProperties() {
109+
FileStorageProperties properties = new FileStorageProperties();
110+
properties.setPath(tempDir.toString());
111+
properties.setUrlPrefix("/uploads");
112+
return properties;
113+
}
114+
115+
private static final class NoopFileStorageService implements FileStorageService {
116+
117+
@Override
118+
public Mono<String> storeFile(FilePart filePart, String subPath) {
119+
throw new UnsupportedOperationException();
120+
}
121+
122+
@Override
123+
public String storeFile(MultipartFile file, String subPath) {
124+
throw new UnsupportedOperationException();
125+
}
126+
127+
@Override
128+
public boolean deleteFile(String filePath) {
129+
throw new UnsupportedOperationException();
130+
}
131+
132+
@Override
133+
public String getFileUrl(String filePath) {
134+
throw new UnsupportedOperationException();
135+
}
136+
137+
@Override
138+
public Resource getFileResource(String filePath) {
139+
throw new UnsupportedOperationException();
140+
}
141+
142+
}
143+
144+
}

0 commit comments

Comments
 (0)