diff --git a/browser/chromium/extract_cookie.go b/browser/chromium/extract_cookie.go index 4f2c04e..c481ae1 100644 --- a/browser/chromium/extract_cookie.go +++ b/browser/chromium/extract_cookie.go @@ -14,7 +14,7 @@ import ( const ( defaultCookieQuery = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, - has_expires, is_persistent FROM cookies` + has_expires, is_persistent, samesite FROM cookies` countCookieQuery = `SELECT COUNT(*) FROM cookies` ) @@ -27,15 +27,27 @@ func extractCookies(masterKeys masterkey.MasterKeys, path string) ([]types.Cooki hasExpire, isPersistent int createdAt, expireAt int64 encryptedValue []byte + sameSite int ) if err := rows.Scan(&name, &encryptedValue, &host, &cookiePath, &createdAt, &expireAt, &isSecure, &isHTTPOnly, - &hasExpire, &isPersistent); err != nil { + &hasExpire, &isPersistent, &sameSite); err != nil { return types.CookieEntry{}, err } value, _ := decryptValue(masterKeys, encryptedValue) value = stripCookieHash(value, host) + sameSiteStr := "unspecified" + switch sameSite { + case 0: + sameSiteStr = "none" + case 1: + sameSiteStr = "lax" + case 2: + sameSiteStr = "strict" + case -1: + // not specified by Set-Cookie + } return types.CookieEntry{ Name: name, Host: host, @@ -47,6 +59,7 @@ func extractCookies(masterKeys masterkey.MasterKeys, path string) ([]types.Cooki IsPersistent: isPersistent != 0, ExpireAt: timeEpoch(expireAt), CreatedAt: timeEpoch(createdAt), + SameSite: sameSiteStr, }, nil }) if err != nil { diff --git a/browser/firefox/extract_cookie.go b/browser/firefox/extract_cookie.go index 1d05918..30f0083 100644 --- a/browser/firefox/extract_cookie.go +++ b/browser/firefox/extract_cookie.go @@ -10,7 +10,7 @@ import ( const ( firefoxCookieQuery = `SELECT name, value, host, path, - creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies` + creationTime, expiry, isSecure, isHttpOnly, sameSite FROM moz_cookies` firefoxCountCookieQuery = `SELECT COUNT(*) FROM moz_cookies` ) @@ -21,12 +21,24 @@ func extractCookies(path string) ([]types.CookieEntry, error) { name, value, host, cookiePath string isSecure, isHTTPOnly int createdAt, expiry int64 + sameSite int ) if err := rows.Scan(&name, &value, &host, &cookiePath, - &createdAt, &expiry, &isSecure, &isHTTPOnly); err != nil { + &createdAt, &expiry, &isSecure, &isHTTPOnly, &sameSite); err != nil { return types.CookieEntry{}, err } hasExpire := expiry > 0 + sameSiteStr := "unspecified" + switch sameSite { + case 0: + sameSiteStr = "none" + case 1: + sameSiteStr = "lax" + case 2: + sameSiteStr = "strict" + case 256: + // not specified by Set-Cookie + } return types.CookieEntry{ Name: name, Host: host, @@ -38,6 +50,7 @@ func extractCookies(path string) ([]types.CookieEntry, error) { IsPersistent: hasExpire, ExpireAt: firefoxSeconds(expiry), CreatedAt: firefoxMicros(createdAt), + SameSite: sameSiteStr, }, nil }) if err != nil { diff --git a/output/cookie_editor.go b/output/cookie_editor.go index 1de4318..ae0bd3d 100644 --- a/output/cookie_editor.go +++ b/output/cookie_editor.go @@ -3,6 +3,7 @@ package output import ( "encoding/json" "io" + "strings" "github.com/moond4rk/hackbrowserdata/types" ) @@ -15,6 +16,8 @@ type cookieEditorFormatter struct { func (f *cookieEditorFormatter) ext() string { return "json" } +var sameSiteNone = "no_restriction" + func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error { if len(rows) == 0 { return nil @@ -32,6 +35,13 @@ func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error { if !c.ExpireAt.IsZero() { expDate = float64(c.ExpireAt.Unix()) } + sameSite := &c.SameSite + switch c.SameSite { + case "none": + sameSite = &sameSiteNone + case "", "unspecified": + sameSite = nil + } entries = append(entries, cookieEditorEntry{ Domain: c.Host, ExpirationDate: expDate, @@ -40,6 +50,9 @@ func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error { Path: c.Path, Secure: c.IsSecure, Value: c.Value, + SameSite: sameSite, + Session: expDate == 0., + HostOnly: !strings.HasPrefix(c.Host, "."), }) } @@ -52,10 +65,13 @@ func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error { // cookieEditorEntry matches the CookieEditor browser extension's import format. type cookieEditorEntry struct { Domain string `json:"domain"` - ExpirationDate float64 `json:"expirationDate"` + ExpirationDate float64 `json:"expirationDate,omitempty"` HTTPOnly bool `json:"httpOnly"` Name string `json:"name"` Path string `json:"path"` Secure bool `json:"secure"` Value string `json:"value"` + SameSite *string `json:"sameSite"` + Session bool `json:"session"` + HostOnly bool `json:"hostOnly"` } diff --git a/output/output_test.go b/output/output_test.go index 6300a56..346c3e7 100644 --- a/output/output_test.go +++ b/output/output_test.go @@ -106,14 +106,14 @@ func TestWrite_CSV_Cookie(t *testing.T) { assert.Equal(t, []string{ "browser", "profile", "host", "path", "name", "value", - "is_secure", "is_http_only", "has_expire", "is_persistent", "expire_at", "created_at", + "is_secure", "is_http_only", "has_expire", "is_persistent", "expire_at", "created_at", "same_site", }, records[0], ) assert.Equal(t, []string{ "Chrome", "Default", ".example.com", "/", "session", "abc123", - "true", "true", "true", "true", "2026-01-15T10:30:00Z", "2026-01-15T10:30:00Z", + "true", "true", "true", "true", "2026-01-15T10:30:00Z", "2026-01-15T10:30:00Z", "", }, records[1], ) diff --git a/output/reflect_test.go b/output/reflect_test.go index e14fdca..1032dfb 100644 --- a/output/reflect_test.go +++ b/output/reflect_test.go @@ -54,7 +54,7 @@ func TestStructCSVHeader(t *testing.T) { expect []string }{ {"LoginEntry", types.LoginEntry{}, []string{"url", "username", "password", "created_at"}}, - {"CookieEntry", types.CookieEntry{}, []string{"host", "path", "name", "value", "is_secure", "is_http_only", "has_expire", "is_persistent", "expire_at", "created_at"}}, + {"CookieEntry", types.CookieEntry{}, []string{"host", "path", "name", "value", "is_secure", "is_http_only", "has_expire", "is_persistent", "expire_at", "created_at", "same_site"}}, {"BookmarkEntry", types.BookmarkEntry{}, []string{"id", "name", "type", "url", "folder", "created_at"}}, {"HistoryEntry", types.HistoryEntry{}, []string{"url", "title", "visit_count", "last_visit"}}, {"DownloadEntry", types.DownloadEntry{}, []string{"url", "target_path", "mime_type", "total_bytes", "start_time", "end_time"}}, @@ -87,7 +87,7 @@ func TestStructCSVRow(t *testing.T) { IsSecure: true, IsHTTPOnly: true, HasExpire: true, IsPersistent: false, ExpireAt: refTime, CreatedAt: refTime, }, - []string{".example.com", "/", "session", "abc", "true", "true", "true", "false", "2026-01-15T10:30:00Z", "2026-01-15T10:30:00Z"}, + []string{".example.com", "/", "session", "abc", "true", "true", "true", "false", "2026-01-15T10:30:00Z", "2026-01-15T10:30:00Z", ""}, }, { "HistoryEntry_int", diff --git a/types/models.go b/types/models.go index 9c9d8f4..7f3864b 100644 --- a/types/models.go +++ b/types/models.go @@ -22,6 +22,7 @@ type CookieEntry struct { IsPersistent bool `json:"is_persistent" csv:"is_persistent"` ExpireAt time.Time `json:"expire_at" csv:"expire_at"` CreatedAt time.Time `json:"created_at" csv:"created_at"` + SameSite string `json:"same_site" csv:"same_site"` } // BookmarkEntry represents a single browser bookmark.