修复 Pages 上传或节点同步时报 pages file size out of bounds:允许 ZIP 包内的 0 字节文件,并兼容未声明解压大小的 ZIP 条目。

This commit is contained in:
ryan
2026-06-21 10:55:48 +08:00
parent 6e86901a58
commit ce2b931a78
5 changed files with 110 additions and 31 deletions
+13 -6
View File
@@ -205,11 +205,19 @@ func extractPagesPackage(packageBytes []byte, releaseDir string, deployment page
return os.Rename(tmpDir, releaseDir)
}
func pagesZipEntryCopyLimit(size uint64) (int64, error) {
if size == 0 || size > pagesMaxExtractedFileBytes || size > uint64(math.MaxInt64) {
func copyPagesZipEntryContent(dst io.Writer, src io.Reader, declaredSize uint64) (int64, error) {
if declaredSize > pagesMaxExtractedFileBytes || declaredSize > uint64(math.MaxInt64) {
return 0, errors.New("pages file size out of bounds")
}
return int64(size), nil //nolint:gosec // size is bounded to math.MaxInt64 above
if declaredSize > 0 {
return io.CopyN(dst, src, int64(declaredSize)) //nolint:gosec // declaredSize is bounded to math.MaxInt64 above
}
limited := io.LimitReader(src, pagesMaxExtractedFileBytes+1)
written, err := io.Copy(dst, limited)
if written > pagesMaxExtractedFileBytes {
return written, errors.New("pages file size out of bounds")
}
return written, err
}
func extractPagesFile(item *zip.File, targetPath string) error {
@@ -226,12 +234,11 @@ func extractPagesFile(item *zip.File, targetPath string) error {
return err
}
defer func() { _ = target.Close() }()
limit, err := pagesZipEntryCopyLimit(item.UncompressedSize64)
_, err = copyPagesZipEntryContent(target, source, item.UncompressedSize64)
if err != nil {
return fmt.Errorf("%s: %w", item.Name, err)
}
_, err = io.CopyN(target, source, limit)
return err
return nil
}
func switchPagesCurrentDir(baseDir string, deploymentID uint, releaseDir string) error {
+43
View File
@@ -237,6 +237,49 @@ func TestSyncOnceDownloadsPagesDeploymentBeforeApply(t *testing.T) {
}
}
func TestSyncOnceExtractsPagesPackageWithZeroByteFiles(t *testing.T) {
packageBytes := testPagesPackage(t, map[string]string{
"index.html": "hello",
".gitkeep": "",
})
checksum := testBytesChecksum(packageBytes)
client := &fakeClient{
config: protocol.ActiveConfigResponse{
Version: "20260309-103",
Checksum: "pages-config-checksum",
SourceConfigJSON: testPagesSourceConfigJSON(9, checksum),
CreatedAt: time.Now().Format(time.RFC3339),
},
pagesPackages: map[uint][]byte{9: packageBytes},
}
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
nodeID, err := stateStore.EnsureNodeID()
if err != nil {
t.Fatalf("EnsureNodeID failed: %v", err)
}
snapshot, _ := stateStore.Load()
snapshot.NodeID = nodeID
if err = stateStore.Save(snapshot); err != nil {
t.Fatalf("save state failed: %v", err)
}
manager := &fakeManager{currentChecksum: "old-checksum"}
service := New(client, manager, stateStore)
pagesDir := t.TempDir()
service.SetPagesDir(pagesDir)
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{Version: "20260309-103", Checksum: "pages-config-checksum"}); err != nil {
t.Fatalf("SyncOnce failed: %v", err)
}
gitkeepPath := filepath.Join(pagesDir, "deployments", "9", "current", ".gitkeep")
info, err := os.Stat(gitkeepPath)
if err != nil {
t.Fatalf("expected zero-byte Pages file to be extracted: %v", err)
}
if info.Size() != 0 {
t.Fatalf("expected zero-byte Pages file, got %d bytes", info.Size())
}
}
func TestSyncOnceRejectsPagesZipSlipBeforeApply(t *testing.T) {
packageBytes := testPagesPackage(t, map[string]string{"../escape.html": "bad", "index.html": "ok"})
checksum := testBytesChecksum(packageBytes)