mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 20:34:39 +08:00
fix(files): isolate generated preview content with sandbox policy
This commit is contained in:
parent
0498530f3a
commit
7e45a26755
@ -60,16 +60,14 @@ public class GeneratedFileController {
|
|||||||
boolean isImage = mime != null && mime.startsWith("image/");
|
boolean isImage = mime != null && mime.startsWith("image/");
|
||||||
boolean isHtml = mime != null && mime.toLowerCase().startsWith("text/html");
|
boolean isHtml = mime != null && mime.toLowerCase().startsWith("text/html");
|
||||||
String disposition = (isImage || isHtml) ? "inline" : "attachment";
|
String disposition = (isImage || isHtml) ? "inline" : "attachment";
|
||||||
if (isHtml) {
|
// Every generated document is untrusted, including SVG served
|
||||||
// The bytes are model/tool-generated HTML served from the app's
|
// inline as an image. Isolate document origins and active content;
|
||||||
// own origin. A strict CSP neutralises XSS: scripts, plugins and
|
// retain static styles/media and explicit downloads for previews.
|
||||||
// framing are forbidden, only inline styles + images/fonts load.
|
headers.add("Content-Security-Policy",
|
||||||
// This makes an on-demand "open the article" preview safe.
|
"sandbox allow-downloads; default-src 'none'; img-src * data:; "
|
||||||
headers.add("Content-Security-Policy",
|
+ "style-src 'unsafe-inline'; font-src * data:; media-src *; "
|
||||||
"default-src 'none'; img-src * data:; style-src 'unsafe-inline'; "
|
+ "base-uri 'none'; form-action 'none'");
|
||||||
+ "font-src * data:; media-src *; base-uri 'none'; form-action 'none'");
|
headers.add("X-Content-Type-Options", "nosniff");
|
||||||
headers.add("X-Content-Type-Options", "nosniff");
|
|
||||||
}
|
|
||||||
headers.add(HttpHeaders.CONTENT_DISPOSITION,
|
headers.add(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
disposition + "; filename=\"" + sanitizeAscii(entry.filename())
|
disposition + "; filename=\"" + sanitizeAscii(entry.filename())
|
||||||
+ "\"; filename*=UTF-8''" + encodedName);
|
+ "\"; filename*=UTF-8''" + encodedName);
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package vip.mate.tool.document;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
@ -12,7 +14,7 @@ import vip.mate.workspace.core.service.WorkspaceService;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ -53,6 +55,33 @@ class GeneratedFileControllerTest {
|
|||||||
assertEquals(200, response.getStatusCode().value());
|
assertEquals(200, response.getStatusCode().value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({"image/svg+xml,inline", "text/html,inline", "image/png,inline", "text/plain,attachment"})
|
||||||
|
void generatedContentHasSandboxPolicyWithoutChangingBytesOrDisposition(String mime, String disposition,
|
||||||
|
@TempDir Path dir) {
|
||||||
|
GeneratedFileCache cache = new GeneratedFileCache(dir);
|
||||||
|
byte[] payload = "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>window.generatedScriptRan=true</script></svg>"
|
||||||
|
.getBytes(StandardCharsets.UTF_8);
|
||||||
|
String id = cache.put(payload, "report", mime, new GeneratedFileCache.Owner(20L, 30L, "conv"));
|
||||||
|
AuthService authService = mock(AuthService.class);
|
||||||
|
WorkspaceService workspaceService = mock(WorkspaceService.class);
|
||||||
|
when(authService.findByUsername("alice")).thenReturn(user(30L, "user"));
|
||||||
|
when(workspaceService.hasPermissionCached(20L, 30L, "viewer")).thenReturn(true);
|
||||||
|
var response = new GeneratedFileController(cache, authService, workspaceService)
|
||||||
|
.download(id, 20L, new TestingAuthenticationToken("alice", "pw"));
|
||||||
|
assertEquals(200, response.getStatusCode().value());
|
||||||
|
assertArrayEquals(payload, (byte[]) response.getBody());
|
||||||
|
assertTrue(response.getHeaders().getFirst("Content-Disposition").startsWith(disposition + ";"));
|
||||||
|
assertEquals("nosniff", response.getHeaders().getFirst("X-Content-Type-Options"));
|
||||||
|
String policy = response.getHeaders().getFirst("Content-Security-Policy");
|
||||||
|
assertNotNull(policy);
|
||||||
|
assertTrue(policy.contains("sandbox allow-downloads;"));
|
||||||
|
assertTrue(policy.contains("default-src 'none';"));
|
||||||
|
assertTrue(policy.contains("form-action 'none'"));
|
||||||
|
assertFalse(policy.contains("allow-scripts"));
|
||||||
|
assertFalse(policy.contains("allow-same-origin"));
|
||||||
|
}
|
||||||
|
|
||||||
private static UserEntity user(Long id, String role) {
|
private static UserEntity user(Long id, String role) {
|
||||||
UserEntity user = new UserEntity();
|
UserEntity user = new UserEntity();
|
||||||
user.setId(id);
|
user.setId(id);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user