Skip to content

Instantly share code, notes, and snippets.

@Tblue
Last active April 24, 2024 21:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tblue/8c6ee7c29b683dcf3e5dad2ff1edc338 to your computer and use it in GitHub Desktop.
Save Tblue/8c6ee7c29b683dcf3e5dad2ff1edc338 to your computer and use it in GitHub Desktop.
Firefox Cookie Dumper
#!/usr/bin/env python3
# vim: tw=120
#
########################################################################################################################
#
# Dump cookies from Firefox, as a Netscape Cookie File.
# Version: 1.0.1
#
# Required third-party Python packages:
#
# - lz4
#
########################################################################################################################
# Copyright 2023 Tilman BLUMENBACH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
# documentation files (the “Software”), to deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
########################################################################################################################
import argparse
import fnmatch
import json
import os
import os.path
import sqlite3
import sys
from collections import namedtuple
import lz4.block
class FirefoxCookieError(Exception):
pass
class FirefoxSessionStoreMissingError(FirefoxCookieError):
pass
# {{{1 mozLz4 decoding
#
# See:
# https://searchfox.org/mozilla-central/rev/05603d404851b5079c20c999890abe2f35a28322/dom/system/IOUtils.h#727
MOZLZ4_MAGIC = b'mozLz40\0'
class InvalidMozLz4FileError(FirefoxCookieError):
pass
def read_mozlz4(file_path):
with open(file_path, 'rb') as fh:
if fh.read(len(MOZLZ4_MAGIC)) != MOZLZ4_MAGIC:
raise InvalidMozLz4FileError(f'not an mozLz4 file: {file_path}')
return lz4.block.decompress(fh.read())
# 1}}}
# {{{1 Cookie serialization
#
# See: https://everything.curl.dev/http/cookies/fileformat
#
# The header of a Netscape Cookie File is one of the following
# (according to http://fileformats.archiveteam.org/wiki/Netscape_cookies.txt):
#
# - "# HTTP Cookie File"
# - "# Netscape HTTP Cookie File"
#
# We choose the second one because the author likes it more.
COOKIE_JAR_HEADER = '# Netscape HTTP Cookie File'
class Cookie(
namedtuple(
'Cookie',
'host allow_subdomains path is_secure expiry name value'
)):
__slots__ = ()
def serialize(self):
return '\t'.join(
str(x) if not isinstance(x, bool) else str(x).upper()
for x in self
)
def write_cookie_jar(cookies, file_path):
with open(
file_path, 'w', encoding='utf-8', newline='\n',
opener=lambda file, flags: os.open(file, flags, mode=0o600)
) as fh:
fh.write(COOKIE_JAR_HEADER)
fh.write('\n')
for c in cookies:
fh.write(c.serialize())
fh.write('\n')
os.chmod(file_path, 0o600)
# 1}}}
class FirefoxCookieReader:
SESSIONSTORE_NAME = 'sessionstore.jsonlz4'
COOKIEDB_NAME = 'cookies.sqlite'
@staticmethod
def _sqlite_convert_bool(bytes_obj):
return bytes_obj != b'0'
def __init__(self, profile_dir, host_glob_patterns=None):
self._profile_dir = profile_dir
self._host_globs = frozenset(host_glob_patterns or [])
sqlite3.register_converter('bool', self._sqlite_convert_bool)
def _is_host_matched(self, host):
if not self._host_globs:
return True
for p in self._host_globs:
if fnmatch.fnmatchcase(host, p):
return True
return False
def _iter_session_cookies(self):
try:
data = json.loads(
read_mozlz4(
os.path.join(self._profile_dir, self.SESSIONSTORE_NAME)
)
)
except OSError as e:
raise FirefoxSessionStoreMissingError(
f'Firefox session store "{e.filename}" not found. Close Firefox, and try again.'
) from e
for c in data['cookies']:
if not self._is_host_matched(c['host']):
continue
yield Cookie(
host=c['host'],
allow_subdomains=c['host'].startswith('.'),
path=c['path'],
is_secure=c.get('secure', False),
expiry=0, # Session cookie
name=c.get('name', ''), # Empty name ok says http.cookiejar (?!)
value=c['value'],
)
def _iter_persisted_cookies(self):
conn = None
try:
conn = sqlite3.connect(
os.path.join(self._profile_dir, self.COOKIEDB_NAME),
detect_types=sqlite3.PARSE_COLNAMES
)
cursor = conn.execute(
'''
SELECT
host,
host LIKE '.%' AS 'allow_subdomains [bool]',
path,
isSecure AS 'isSecure [bool]',
expiry,
name,
value
FROM moz_cookies;
'''
)
for row in cursor:
c = Cookie._make(row)
if not self._is_host_matched(c.host):
continue
yield c
finally:
if conn:
conn.close()
def __iter__(self):
yield from self._iter_session_cookies()
yield from self._iter_persisted_cookies()
def get_argparser():
p = argparse.ArgumentParser(
description='Dump cookies from Firefox, as a Netscape Cookie File.'
)
p.add_argument(
'-l', '--host-limit',
metavar='GLOB_PATTERN',
action='append',
help='Only dump cookies for hosts matching the glob pattern. Can be specified multiple times. Example: '
'*.example.com'
)
p.add_argument(
'firefox_profile_dir',
help='Path to Firefox profile directory.'
)
p.add_argument(
'output_file',
help='Path to the output file.'
)
return p
def main():
args = get_argparser().parse_args()
cookie_reader = FirefoxCookieReader(args.firefox_profile_dir, args.host_limit)
write_cookie_jar(cookie_reader, args.output_file)
if __name__ == '__main__':
sys.exit(main())
@pokemaster974
Copy link

pokemaster974 commented Feb 24, 2024

Hi, is using "sessionstore.jsonlz4" mandatory ?
I would like to use your python script to retrieve cookies but without closing Firefox.
Regards.

Edit : I've seen that there is a backup folder (sessionstore-backups), I've modify the script like this
SESSIONSTORE_NAME = SESSIONSTOREBACKUPS_FOLDER + '\\' + 'recovery.jsonlz4'
Do you think it could be a "correct" workaround to get the cookies without closing Firefox ?

@Tblue
Copy link
Author

Tblue commented Feb 27, 2024

@pokemaster974:

Hi, is using "sessionstore.jsonlz4" mandatory ?

No. However, if you skip it, you won't get any session cookies. That might or might not be an issue, depending on your use case.

I would like to use your python script to retrieve cookies but without closing Firefox.

An issue with this might be that cookies.sqlite is locked by Firefox while it's running. Copying the DB somewhere else and reading from the copy might work around that, but it's probably not easily done on Windows (where the file can't apparently easily be copied as long as it's opened by Firefox).

Edit : I've seen that there is a backup folder (sessionstore-backups), I've modify the script like this
SESSIONSTORE_NAME = SESSIONSTOREBACKUPS_FOLDER + '\' + 'recovery.jsonlz4'
Do you think it could be a "correct" workaround to get the cookies without closing Firefox ?

If that works for you, that's great! However, be aware that the session cookies you get might not be the latest ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment