@@ -171,6 +171,39 @@ func TestEngineDeniesDaemonTokenFileTools(t *testing.T) {
171171 }
172172}
173173
174+ func TestApplyPatchDeniesQuotedDaemonTokenPathWithSpaces (t * testing.T ) {
175+ ws , err := filepath .EvalSymlinks (t .TempDir ())
176+ if err != nil {
177+ t .Fatalf ("EvalSymlinks: %v" , err )
178+ }
179+ token := filepath .Join (ws , "bridge token" )
180+ if err := os .WriteFile (token , []byte ("bridge-secret\n " ), 0o600 ); err != nil {
181+ t .Fatalf ("write token: %v" , err )
182+ }
183+ t .Setenv (remote .EnvToken , "" )
184+ t .Setenv (remote .EnvTokenFile , token )
185+ engine := sandbox .NewEngine (sandbox.EngineOptions {WorkspaceRoot : ws , Policy : sandbox .DefaultPolicy ()})
186+
187+ registry := NewRegistry ()
188+ registry .Register (NewScopedApplyPatchTool (ws , nil ))
189+ patch := "diff --git \" a/bridge token\" \" b/bridge token\" \n " +
190+ "--- \" a/bridge token\" \n " +
191+ "+++ \" b/bridge token\" \n " +
192+ "@@ -1 +1 @@\n " +
193+ "-bridge-secret\n " +
194+ "+attacker-controlled\n "
195+ result := registry .RunWithOptions (context .Background (), "apply_patch" , map [string ]any {
196+ "patch" : patch ,
197+ }, RunOptions {Sandbox : engine , PermissionGranted : true })
198+ if result .Status == StatusOK || ! strings .Contains (result .Output , "remote bridge token" ) {
199+ t .Fatalf ("apply_patch on quoted protected path: status=%s output=%q, want bridge-token denial" , result .Status , result .Output )
200+ }
201+ contents , err := os .ReadFile (token )
202+ if err != nil || string (contents ) != "bridge-secret\n " {
203+ t .Fatalf ("token changed after denied patch: contents=%q err=%v" , contents , err )
204+ }
205+ }
206+
174207// TestDaemonTokenAliasesDeniedEndToEnd covers the in-process tools, which are
175208// the layer that can close inode aliases: they see every requested path before
176209// opening it, so a symlink or hard link to the token resolves back to the
0 commit comments