mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-08-15 23:50:19 +02:00
feat: Add support for sameSite field (#628)
Recent browsers (and HTTP) added "sameSite" setting to limit cross site attacks. Add support for exporting this property from Firefox and Chrome and to export to csv and cookie-editor format. Also extract new "session" and "hostOnly" properties for cookie-editor format. Tested on Firefox, Chrome and Chromium. Signed-off-by: Frediano Ziglio <freddy77@gmail.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
defaultCookieQuery = `SELECT name, encrypted_value, host_key, path,
|
defaultCookieQuery = `SELECT name, encrypted_value, host_key, path,
|
||||||
creation_utc, expires_utc, is_secure, is_httponly,
|
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`
|
countCookieQuery = `SELECT COUNT(*) FROM cookies`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,15 +27,27 @@ func extractCookies(masterKeys masterkey.MasterKeys, path string) ([]types.Cooki
|
|||||||
hasExpire, isPersistent int
|
hasExpire, isPersistent int
|
||||||
createdAt, expireAt int64
|
createdAt, expireAt int64
|
||||||
encryptedValue []byte
|
encryptedValue []byte
|
||||||
|
sameSite int
|
||||||
)
|
)
|
||||||
if err := rows.Scan(&name, &encryptedValue, &host, &cookiePath,
|
if err := rows.Scan(&name, &encryptedValue, &host, &cookiePath,
|
||||||
&createdAt, &expireAt, &isSecure, &isHTTPOnly,
|
&createdAt, &expireAt, &isSecure, &isHTTPOnly,
|
||||||
&hasExpire, &isPersistent); err != nil {
|
&hasExpire, &isPersistent, &sameSite); err != nil {
|
||||||
return types.CookieEntry{}, err
|
return types.CookieEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
value, _ := decryptValue(masterKeys, encryptedValue)
|
value, _ := decryptValue(masterKeys, encryptedValue)
|
||||||
value = stripCookieHash(value, host)
|
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{
|
return types.CookieEntry{
|
||||||
Name: name,
|
Name: name,
|
||||||
Host: host,
|
Host: host,
|
||||||
@@ -47,6 +59,7 @@ func extractCookies(masterKeys masterkey.MasterKeys, path string) ([]types.Cooki
|
|||||||
IsPersistent: isPersistent != 0,
|
IsPersistent: isPersistent != 0,
|
||||||
ExpireAt: timeEpoch(expireAt),
|
ExpireAt: timeEpoch(expireAt),
|
||||||
CreatedAt: timeEpoch(createdAt),
|
CreatedAt: timeEpoch(createdAt),
|
||||||
|
SameSite: sameSiteStr,
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
firefoxCookieQuery = `SELECT name, value, host, path,
|
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`
|
firefoxCountCookieQuery = `SELECT COUNT(*) FROM moz_cookies`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,12 +21,24 @@ func extractCookies(path string) ([]types.CookieEntry, error) {
|
|||||||
name, value, host, cookiePath string
|
name, value, host, cookiePath string
|
||||||
isSecure, isHTTPOnly int
|
isSecure, isHTTPOnly int
|
||||||
createdAt, expiry int64
|
createdAt, expiry int64
|
||||||
|
sameSite int
|
||||||
)
|
)
|
||||||
if err := rows.Scan(&name, &value, &host, &cookiePath,
|
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
|
return types.CookieEntry{}, err
|
||||||
}
|
}
|
||||||
hasExpire := expiry > 0
|
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{
|
return types.CookieEntry{
|
||||||
Name: name,
|
Name: name,
|
||||||
Host: host,
|
Host: host,
|
||||||
@@ -38,6 +50,7 @@ func extractCookies(path string) ([]types.CookieEntry, error) {
|
|||||||
IsPersistent: hasExpire,
|
IsPersistent: hasExpire,
|
||||||
ExpireAt: firefoxSeconds(expiry),
|
ExpireAt: firefoxSeconds(expiry),
|
||||||
CreatedAt: firefoxMicros(createdAt),
|
CreatedAt: firefoxMicros(createdAt),
|
||||||
|
SameSite: sameSiteStr,
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+17
-1
@@ -3,6 +3,7 @@ package output
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/types"
|
"github.com/moond4rk/hackbrowserdata/types"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,8 @@ type cookieEditorFormatter struct {
|
|||||||
|
|
||||||
func (f *cookieEditorFormatter) ext() string { return "json" }
|
func (f *cookieEditorFormatter) ext() string { return "json" }
|
||||||
|
|
||||||
|
var sameSiteNone = "no_restriction"
|
||||||
|
|
||||||
func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error {
|
func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error {
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -32,6 +35,13 @@ func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error {
|
|||||||
if !c.ExpireAt.IsZero() {
|
if !c.ExpireAt.IsZero() {
|
||||||
expDate = float64(c.ExpireAt.Unix())
|
expDate = float64(c.ExpireAt.Unix())
|
||||||
}
|
}
|
||||||
|
sameSite := &c.SameSite
|
||||||
|
switch c.SameSite {
|
||||||
|
case "none":
|
||||||
|
sameSite = &sameSiteNone
|
||||||
|
case "", "unspecified":
|
||||||
|
sameSite = nil
|
||||||
|
}
|
||||||
entries = append(entries, cookieEditorEntry{
|
entries = append(entries, cookieEditorEntry{
|
||||||
Domain: c.Host,
|
Domain: c.Host,
|
||||||
ExpirationDate: expDate,
|
ExpirationDate: expDate,
|
||||||
@@ -40,6 +50,9 @@ func (f *cookieEditorFormatter) format(w io.Writer, rows []row) error {
|
|||||||
Path: c.Path,
|
Path: c.Path,
|
||||||
Secure: c.IsSecure,
|
Secure: c.IsSecure,
|
||||||
Value: c.Value,
|
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.
|
// cookieEditorEntry matches the CookieEditor browser extension's import format.
|
||||||
type cookieEditorEntry struct {
|
type cookieEditorEntry struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
ExpirationDate float64 `json:"expirationDate"`
|
ExpirationDate float64 `json:"expirationDate,omitempty"`
|
||||||
HTTPOnly bool `json:"httpOnly"`
|
HTTPOnly bool `json:"httpOnly"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Secure bool `json:"secure"`
|
Secure bool `json:"secure"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
SameSite *string `json:"sameSite"`
|
||||||
|
Session bool `json:"session"`
|
||||||
|
HostOnly bool `json:"hostOnly"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,14 +106,14 @@ func TestWrite_CSV_Cookie(t *testing.T) {
|
|||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{
|
[]string{
|
||||||
"browser", "profile", "host", "path", "name", "value",
|
"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],
|
records[0],
|
||||||
)
|
)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{
|
[]string{
|
||||||
"Chrome", "Default", ".example.com", "/", "session", "abc123",
|
"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],
|
records[1],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestStructCSVHeader(t *testing.T) {
|
|||||||
expect []string
|
expect []string
|
||||||
}{
|
}{
|
||||||
{"LoginEntry", types.LoginEntry{}, []string{"url", "username", "password", "created_at"}},
|
{"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"}},
|
{"BookmarkEntry", types.BookmarkEntry{}, []string{"id", "name", "type", "url", "folder", "created_at"}},
|
||||||
{"HistoryEntry", types.HistoryEntry{}, []string{"url", "title", "visit_count", "last_visit"}},
|
{"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"}},
|
{"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,
|
IsSecure: true, IsHTTPOnly: true, HasExpire: true, IsPersistent: false,
|
||||||
ExpireAt: refTime, CreatedAt: refTime,
|
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",
|
"HistoryEntry_int",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type CookieEntry struct {
|
|||||||
IsPersistent bool `json:"is_persistent" csv:"is_persistent"`
|
IsPersistent bool `json:"is_persistent" csv:"is_persistent"`
|
||||||
ExpireAt time.Time `json:"expire_at" csv:"expire_at"`
|
ExpireAt time.Time `json:"expire_at" csv:"expire_at"`
|
||||||
CreatedAt time.Time `json:"created_at" csv:"created_at"`
|
CreatedAt time.Time `json:"created_at" csv:"created_at"`
|
||||||
|
SameSite string `json:"same_site" csv:"same_site"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BookmarkEntry represents a single browser bookmark.
|
// BookmarkEntry represents a single browser bookmark.
|
||||||
|
|||||||
Reference in New Issue
Block a user