@@ -419,3 +419,82 @@ func TestPicoD_SetWorkspace(t *testing.T) {
419419 require .NoError (t , err )
420420 assert .Equal (t , resolve (absLinkPath ), resolve (server .workspaceDir ))
421421}
422+
423+ func TestPicoD_SymlinkUploadGuard (t * testing.T ) {
424+ // 1. Setup Keys
425+ routerPriv , routerPubStr := generateRSAKeys (t )
426+
427+ // 2. Setup Server
428+ _ , ts , tmpDir := setupTestServer (t , routerPubStr )
429+ defer os .RemoveAll (tmpDir )
430+ defer ts .Close ()
431+ defer os .Unsetenv (PublicKeyEnvVar )
432+
433+ client := ts .Client ()
434+
435+ // Helper to create auth headers
436+ getAuthHeaders := func () http.Header {
437+ claims := jwt.MapClaims {
438+ "iat" : time .Now ().Unix (),
439+ "exp" : time .Now ().Add (time .Hour * 6 ).Unix (),
440+ }
441+ token := createToken (t , routerPriv , claims )
442+
443+ h := make (http.Header )
444+ h .Set ("Authorization" , "Bearer " + token )
445+ return h
446+ }
447+
448+ // 3. Plant a symlink inside the workspace that points outside it
449+ outsideDir := t .TempDir ()
450+ symlinkPath := filepath .Join (tmpDir , "escape-link" )
451+ require .NoError (t , os .Symlink (outsideDir , symlinkPath ))
452+
453+ // 4. Test JSON Base64 Upload through symlink (should fail)
454+ t .Run ("JSON Upload Symlink Guard" , func (t * testing.T ) {
455+ contentB64 := base64 .StdEncoding .EncodeToString ([]byte ("malicious content" ))
456+ uploadReq := UploadFileRequest {
457+ Path : "escape-link/newdir/malicious.txt" ,
458+ Content : contentB64 ,
459+ Mode : "0644" ,
460+ }
461+ body , _ := json .Marshal (uploadReq )
462+
463+ req , _ := http .NewRequest ("POST" , ts .URL + "/api/files" , bytes .NewBuffer (body ))
464+ req .Header = getAuthHeaders ()
465+ req .Header .Set ("Content-Type" , "application/json" )
466+ resp , err := client .Do (req )
467+ require .NoError (t , err )
468+ assert .Equal (t , http .StatusInternalServerError , resp .StatusCode )
469+
470+ // Verify file was NOT created in outsideDir
471+ outsideFile := filepath .Join (outsideDir , "newdir/malicious.txt" )
472+ _ , statErr := os .Stat (outsideFile )
473+ assert .True (t , os .IsNotExist (statErr ), "File should not be created outside workspace" )
474+ })
475+
476+ // 5. Test Multipart Upload through symlink (should fail)
477+ t .Run ("Multipart Upload Symlink Guard" , func (t * testing.T ) {
478+ bodyBuf := & bytes.Buffer {}
479+ writer := multipart .NewWriter (bodyBuf )
480+ part , _ := writer .CreateFormFile ("file" , "malicious_multipart.txt" )
481+ _ , err := part .Write ([]byte ("multipart malicious content" ))
482+ require .NoError (t , err )
483+ err = writer .WriteField ("path" , "escape-link/newdir/malicious_multipart.txt" )
484+ require .NoError (t , err )
485+ writer .Close ()
486+
487+ req , _ := http .NewRequest ("POST" , ts .URL + "/api/files" , bodyBuf )
488+ req .Header = getAuthHeaders ()
489+ req .Header .Set ("Content-Type" , writer .FormDataContentType ())
490+ resp , err := client .Do (req )
491+ require .NoError (t , err )
492+ assert .Equal (t , http .StatusInternalServerError , resp .StatusCode )
493+
494+ // Verify file was NOT created in outsideDir
495+ outsideFile := filepath .Join (outsideDir , "newdir/malicious_multipart.txt" )
496+ _ , statErr := os .Stat (outsideFile )
497+ assert .True (t , os .IsNotExist (statErr ), "File should not be created outside workspace" )
498+ })
499+ }
500+
0 commit comments