Skip to content

Commit 0276a86

Browse files
committed
test(picod): add regression coverage for symlink traversal in upload handlers
Signed-off-by: Abhinav Singh <abhinavsingh717073@gmail.com>
1 parent 53cc8b6 commit 0276a86

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

pkg/picod/picod_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)