返回 DeepSeek-Reasonix
upload_test.go
根目录 / internal / browser / cdp / upload_test.go
1 package cdp
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/browser"
11 )
12
13 func uploadRequest(tab browser.Tab, token, op string, files ...string) browser.ActRequest {
14 return browser.ActRequest{
15 OperationID: op, TabID: tab.ID, DocumentToken: token,
16 Action: browser.ActionUpload, Ref: "e1", Files: files,
17 }
18 }
19
20 // A page is untrusted: a file input that accepted any path would hand the
21 // site every file this process can read.
22 func TestUploadRefusesFilesOutsideTheTaskDirectories(t *testing.T) {
23 workspace := t.TempDir()
24 outside := t.TempDir()
25 owned := filepath.Join(workspace, "resume.pdf")
26 secret := filepath.Join(outside, "id_rsa")
27 for _, path := range []string{owned, secret} {
28 if err := os.WriteFile(path, []byte("x"), 0o600); err != nil {
29 t.Fatalf("write %s: %v", path, err)
30 }
31 }
32 f := newFakeBrowser(t)
33 exec := newTestExecutorWithRoots(t, f, workspace)
34 ctx := context.Background()
35 tab, snap := openTab(t, exec, ctx)
36
37 res, err := exec.Act(ctx, uploadRequest(tab, snap.DocumentToken, "op-secret", secret))
38 if err != nil {
39 t.Fatalf("refused upload returned an error: %v", err)
40 }
41 if res.Executed {
42 t.Fatal("a file outside the task's directories was attached to a page")
43 }
44 if !strings.Contains(res.Reason, "outside this task's directories") {
45 t.Fatalf("reason = %q, want the containment refusal", res.Reason)
46 }
47 if f.countCalls("DOM.setFileInputFiles") != 0 {
48 t.Fatal("the refused path still reached the browser")
49 }
50 if res, err = exec.Act(ctx, uploadRequest(tab, snap.DocumentToken, "op-owned", owned)); err != nil || !res.Executed {
51 t.Fatalf("a workspace file was refused: %+v %v", res, err)
52 }
53 }
54
55 // EvalSymlinks runs before the containment check, so a link the agent can
56 // write inside the workspace cannot point a file input at a private key.
57 func TestUploadRefusesASymlinkEscape(t *testing.T) {
58 workspace := t.TempDir()
59 outside := t.TempDir()
60 secret := filepath.Join(outside, "id_rsa")
61 if err := os.WriteFile(secret, []byte("key"), 0o600); err != nil {
62 t.Fatalf("write secret: %v", err)
63 }
64 link := filepath.Join(workspace, "innocent.txt")
65 if err := os.Symlink(secret, link); err != nil {
66 t.Skipf("symlinks unavailable: %v", err)
67 }
68 f := newFakeBrowser(t)
69 exec := newTestExecutorWithRoots(t, f, workspace)
70 ctx := context.Background()
71 tab, snap := openTab(t, exec, ctx)
72
73 res, err := exec.Act(ctx, uploadRequest(tab, snap.DocumentToken, "op-link", link))
74 if err != nil {
75 t.Fatalf("refused upload returned an error: %v", err)
76 }
77 if res.Executed {
78 t.Fatal("a symlink out of the workspace was attached to a page")
79 }
80 if f.countCalls("DOM.setFileInputFiles") != 0 {
81 t.Fatal("the symlink still reached the browser")
82 }
83 }
84
85 // A download the agent just took is inside the executor's own directory and
86 // must stay attachable without configuring anything.
87 func TestUploadAllowsTheExecutorsOwnArtifacts(t *testing.T) {
88 f := newFakeBrowser(t)
89 exec := newTestExecutorWithRoots(t, f)
90 ctx := context.Background()
91 tab, snap := openTab(t, exec, ctx)
92
93 downloaded := filepath.Join(exec.artifacts, "downloads", "report.csv")
94 if err := os.WriteFile(downloaded, []byte("id\n"), 0o600); err != nil {
95 t.Fatalf("stage download: %v", err)
96 }
97 res, err := exec.Act(ctx, uploadRequest(tab, snap.DocumentToken, "op-artifact", downloaded))
98 if err != nil || !res.Executed {
99 t.Fatalf("a downloaded file was refused: %+v %v", res, err)
100 }
101 }
102
103 func TestUploadRefusesDirectoriesAndMissingFiles(t *testing.T) {
104 workspace := t.TempDir()
105 f := newFakeBrowser(t)
106 exec := newTestExecutorWithRoots(t, f, workspace)
107 ctx := context.Background()
108 tab, snap := openTab(t, exec, ctx)
109
110 for op, path := range map[string]string{
111 "op-dir": workspace,
112 "op-missing": filepath.Join(workspace, "absent.txt"),
113 } {
114 res, err := exec.Act(ctx, uploadRequest(tab, snap.DocumentToken, op, path))
115 if err != nil || res.Executed {
116 t.Fatalf("%s: %+v %v, want a refusal", op, res, err)
117 }
118 }
119 }
120
121 func TestArtifactPathRefusesNamesThatAreNotLeaves(t *testing.T) {
122 f := newFakeBrowser(t)
123 exec := newTestExecutorWithRoots(t, f)
124 for _, name := range []string{"../escape.png", "sub/shot.png", `..\escape.png`} {
125 if _, err := exec.artifactPath("screenshots", name); err == nil {
126 t.Fatalf("artifactPath accepted %q", name)
127 }
128 }
129 path, err := exec.artifactPath("screenshots", "tab-1-1.png")
130 if err != nil || filepath.Dir(path) != filepath.Join(exec.artifacts, "screenshots") {
131 t.Fatalf("artifactPath(%q) = %q, %v", "tab-1-1.png", path, err)
132 }
133 }
134
134 lines GO