mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-08-11 23:30:19 +02:00
fix(archive): correct flat-layout path and entry-count wording
Drop the phantom <basename>/ level for flat-layout installs (profileDir == root), say "entries" not "files" (count is per source path), and add ZipDir/Unzip (>1MB, empty, Zip-Slip) + flat-layout tests.
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ type Archivable interface {
|
|||||||
// BuildArchive packs each browser's decryption-relevant files into a zip whose internal layout is
|
// BuildArchive packs each browser's decryption-relevant files into a zip whose internal layout is
|
||||||
// <browser-key>/<User Data layout>, so a restore can re-expand it and decrypt with a keys.json. Files
|
// <browser-key>/<User Data layout>, so a restore can re-expand it and decrypt with a keys.json. Files
|
||||||
// are staged through a locked-file session first because Windows holds exclusive SQLite locks. Returns
|
// are staged through a locked-file session first because Windows holds exclusive SQLite locks. Returns
|
||||||
// the number of files captured.
|
// the number of source entries staged (a directory source counts once).
|
||||||
func BuildArchive(browsers []Browser, categories []types.Category, outPath string) (int, error) {
|
func BuildArchive(browsers []Browser, categories []types.Category, outPath string) (int, error) {
|
||||||
session, err := filemanager.NewSession()
|
session, err := filemanager.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ func (b *Browser) ArchiveSources(categories []types.Category) []ArchiveSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, p := range b.profiles {
|
for _, p := range b.profiles {
|
||||||
profileRel := filepath.Base(p.profileDir)
|
// Flat-layout installs hold data directly under UserDataDir (profileDir == root); skip the
|
||||||
|
// basename so the archive matches the real layout instead of inserting a phantom level.
|
||||||
|
profileRel := ""
|
||||||
|
if p.profileDir != b.cfg.UserDataDir {
|
||||||
|
profileRel = filepath.Base(p.profileDir)
|
||||||
|
}
|
||||||
for _, marker := range profileMarkers {
|
for _, marker := range profileMarkers {
|
||||||
abs := filepath.Join(p.profileDir, marker)
|
abs := filepath.Join(p.profileDir, marker)
|
||||||
if fileutil.FileExists(abs) {
|
if fileutil.FileExists(abs) {
|
||||||
|
|||||||
@@ -56,3 +56,33 @@ func TestArchiveSources_ForwardSlashLayout(t *testing.T) {
|
|||||||
t.Errorf("missing Local State entry (User Data root file), got %+v", srcs)
|
t.Errorf("missing Local State entry (User Data root file), got %+v", srcs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArchiveSources_FlatLayoutNoExtraLevel(t *testing.T) {
|
||||||
|
// Flat-layout install: data lives directly under UserDataDir with no Default/ subdir, so
|
||||||
|
// discoverProfiles falls back to UserDataDir itself as the profile (profileDir == root).
|
||||||
|
udd := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(udd, "History"), []byte("x"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := NewBrowser(types.BrowserConfig{Key: "opera", Name: "opera", Kind: types.Chromium, UserDataDir: udd})
|
||||||
|
if err != nil || b == nil {
|
||||||
|
t.Fatalf("NewBrowser: b=%v err=%v", b, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcs := b.ArchiveSources([]types.Category{types.History})
|
||||||
|
|
||||||
|
phantom := filepath.Base(udd) + "/"
|
||||||
|
var gotHistory bool
|
||||||
|
for _, s := range srcs {
|
||||||
|
if strings.HasPrefix(s.LayoutRel, phantom) {
|
||||||
|
t.Errorf("flat layout must not insert a %q level, got %q", phantom, s.LayoutRel)
|
||||||
|
}
|
||||||
|
if s.LayoutRel == "History" {
|
||||||
|
gotHistory = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !gotHistory {
|
||||||
|
t.Errorf("expected History at archive root, got %+v", srcs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func archiveCmd() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Infof("Archived %d file(s) to %s", n, outputPath)
|
log.Infof("Archived %d entries to %s", n, outputPath)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package fileutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZipDirUnzip_RoundTrip(t *testing.T) {
|
||||||
|
src := t.TempDir()
|
||||||
|
files := map[string][]byte{
|
||||||
|
"empty.txt": {},
|
||||||
|
"small.txt": []byte("hello"),
|
||||||
|
"Default/Network/Cookies": []byte("cookie-bytes"),
|
||||||
|
"sub/big.bin": bytes.Repeat([]byte("A"), 3<<20), // 3 MiB: exercises the chunked copy loop
|
||||||
|
}
|
||||||
|
for rel, data := range files {
|
||||||
|
p := filepath.Join(src, filepath.FromSlash(rel))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(p, data, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zipPath := filepath.Join(t.TempDir(), "out.zip")
|
||||||
|
if err := ZipDir(zipPath, src); err != nil {
|
||||||
|
t.Fatalf("ZipDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zr, err := zip.OpenReader(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open zip: %v", err)
|
||||||
|
}
|
||||||
|
for _, f := range zr.File {
|
||||||
|
if strings.Contains(f.Name, `\`) {
|
||||||
|
t.Errorf("zip entry name must be forward-slash, got %q", f.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := zr.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := t.TempDir()
|
||||||
|
if err := Unzip(zipPath, dst); err != nil {
|
||||||
|
t.Fatalf("Unzip: %v", err)
|
||||||
|
}
|
||||||
|
for rel, want := range files {
|
||||||
|
got, err := os.ReadFile(filepath.Join(dst, filepath.FromSlash(rel)))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("missing %s after Unzip: %v", rel, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("%s: content mismatch (got %d bytes, want %d)", rel, len(got), len(want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnzip_RejectsZipSlip(t *testing.T) {
|
||||||
|
zipPath := filepath.Join(t.TempDir(), "evil.zip")
|
||||||
|
f, err := os.Create(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
w, err := zw.Create("../escape.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte("pwned")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Unzip(zipPath, t.TempDir()); err == nil {
|
||||||
|
t.Fatal("Unzip must reject an entry that escapes the destination")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user