Skip to main content

cookiespec

version python license deps tests

"The only RFC 6265-correct Set-Cookie / Cookie header parser, validator, and linter for Python — strict, deterministic, zero dependencies."

http.cookies is stuck on RFC 2109 and silently mis-parses modern headers (no SameSite, comma-as-separator bug, wrong Expires format, treats version=4 as load-bearing). cookiespec parses, validates, and round-trips Set-Cookie and Cookie headers strictly per RFC 6265 (with SameSite from RFC 6265bis) — with a small, dependency-free API and a CLI for security audits.

Quick Start

pip install git+https://github.com/prasad-a-abhishek/cookiespec.git

Requires Python ≥ 3.11. Zero runtime dependencies. (The package is not on PyPI yet; install from GitHub until the first public release ships.)

from cookiespec import parse_set_cookie, parse_cookie_header, format_set_cookie
from cookiespec import SameSite, validate_set_cookie

sess = parse_set_cookie("session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600")
sess.name            # 'session'
sess.value           # 'abc123'
sess.attrs.secure    # True
sess.attrs.samesite  # SameSite.LAX
sess.attrs.max_age   # 3600
sess.expires_at      # datetime | None  (one hour in the future)

jar = parse_cookie_header("session=abc123; theme=dark")
{str(k): str(v) for k, v in jar.items()}  # {'session': 'abc123', 'theme': 'dark'}

format_set_cookie(sess)
# 'session=abc123; Expires=...; Max-Age=3600; Domain=...; Path=/; Secure; HttpOnly; SameSite=Lax'

report = validate_set_cookie("a=b; domain=evil.com; path=/; max-age=-1")
report.issues
# [Issue('COOKIE_004', 'Max-Age is negative'), Issue('COOKIE_022', ...)]
$ python -m cookiespec parse --kind set-cookie 'session=abc; Path=/; Secure; HttpOnly; SameSite=Lax'
{"name":"session","value":"abc","path":"/","secure":true,"httponly":true,"samesite":"Lax"}

$ python -m cookiespec lint --strict app.har.json
app.har.json: 17 issue(s)
COOKIE_001 missing Secure   response:api.example.com  Set-Cookie: tracker=...
COOKIE_002 missing HttpOnly response:api.example.com  Set-Cookie: tracker=...
COOKIE_003 missing SameSite response:api.example.com  Set-Cookie: tracker=...
COOKIE_007 missing Path=/   response:api.example.com  Set-Cookie: tracker=...

⚡ Performance & Benchmarks

50-iteration head-to-head benchmark (10 workload profiles × 5 runs each = 250 samples per cell) comparing cookiespec.parse_set_cookie against http.cookies.SimpleCookie.load across modern Set-Cookie patterns. Reproducible via python3 benchmarks/run_benchmark.py.

Workload cookiespec (median µs) http.cookies (median µs) speedup result
minimal_set_cookie 3.7 4.8 1.3× OK
full_security_attrs 7.1 10.6 1.5× OK
samesite_variants 6.8 12.8 1.9× OK
max_age_precedence 9.0 7.1 0.8× OK
expires_rfc1123 8.0 5.9 0.7× OK
expires_asctime 8.5 3.6 0.4× SILENT-SWALLOW²
expires_rfc850 8.5 3.9 0.5× SILENT-SWALLOW²
quoted_value_with_comma 6.2 8.7 1.4× OK
control_chars_rejected 2.1 SILENT-SWALLOW¹
many_attributes 12.9 14.8 1.1× OK
  1. http.cookies silently swallows cookies with control characters (CPython #61542) — the cookie parses successfully but the dict is empty.
  2. http.cookies does not parse RFC 850 or asctime Expires formats (psf/requests #6004) — the cookie parses but the Expires attribute is silently dropped.

cookiespec raises deterministic ValueErrors on control characters and parses all three RFC 6265 §4.1.1 Expires formats faithfully. Speedup on real-world security-aware workloads (full_security_attrs, samesite_variants, quoted_value_with_comma) ranges from 1.4× to 1.9×; the stdlib wins on the three purely-timing-only date-formats because it skips the parse entirely (by silently dropping them).

Why cookiespec?

The Python standard library http.cookies module is stuck on the RFC 2109 (1997) grammar. Six independent signals of pain:

Source Issue
CPython #92012 http.cookies is still documented as RFC 2109-based; modern headers mis-parse (SimpleCookie().load('version=4') returns empty)
CPython #61542 Stdlib parser either swallows malformed cookies or raises on inputs browsers accept
CPython #88629 http.cookiejar does not parse SameSite — the CSRF-defence baseline since Chrome 80 (2020)
requests #6004 requests/cookies.py inherits an RFC-incompatible Expires date template
SO #22493000 Set-Cookie: c2=value, c1=value; path=/; secure; httponly parses as only one cookie with no flags
tornado #2573 Tornado maintainers fork the stdlib cookie layer to add SameSite

cookiespec is the only Python library that targets RFC 6265 directly, ships a strict validator with stable COOKIE_NNN rule codes, exposes a CLI for security audits, and has zero runtime dependencies.

Key Features & API Reference

Public symbols

parse_set_cookie(header: str, *, request_uri: str | None = None) -> Cookie
parse_cookie_header(header: str) -> dict[str, str]
format_set_cookie(cookie: Cookie) -> str
validate_set_cookie(header_or_cookie: str | Cookie, *, strict: bool = False, strict_bis: bool = True) -> Report
default_path(request_uri: str) -> str
parse_date(value: str) -> datetime | None

Value objects

@dataclass(frozen=True)
class Cookie:
    name: str
    value: str          # raw octets, never URL-decoded
    attrs: CookieAttrs
    raw: str            # original header (whitespace preserved)
    expires_at: datetime | None   # effective expiry (Max-Age wins over Expires)

@dataclass(frozen=True)
class CookieAttrs:
    expires: datetime | None = None
    max_age: int | None = None
    domain: str | None = None
    path: str | None = None
    secure: bool = False
    httponly: bool = False
    samesite: SameSite | None = None
    extra: tuple[tuple[str, str], ...] = ()

class SameSite(str, Enum):
    STRICT = "Strict"
    LAX = "Lax"
    NONE = "None"

@dataclass(frozen=True)
class Issue:
    code: str           # COOKIE_NNN — stable across releases
    message: str
    severity: str       # "error" | "warning" | "info"
    location: str | None

@dataclass(frozen=True)
class Report:
    cookie: Cookie
    issues: tuple[Issue, ...]
    strict: bool
    strict_bis: bool
    @property
    def ok(self) -> bool
    @property
    def warnings(self) -> tuple[Issue, ...]
    @property
    def errors(self) -> tuple[Issue, ...]

Validator rule codes (stable public contract)

Code Severity Meaning
COOKIE_001 warning missing Secure attribute
COOKIE_002 warning missing HttpOnly for a session-style cookie
COOKIE_003 warning missing SameSite attribute
COOKIE_004 warning negative Max-Age
COOKIE_005 warning cookie value contains an unquoted comma
COOKIE_006 info Domain has no leading dot (RFC 2109 quirk)
COOKIE_007 warning Path does not start with /
COOKIE_008 warning Expires not parseable
COOKIE_009 error cookie name is empty
COOKIE_010 warning duplicate attribute
COOKIE_011 warning SameSite value is not Strict/Lax/None
COOKIE_012 warning SameSite value is empty
COOKIE_013 error SameSite=None without Secure (RFC 6265bis)
COOKIE_014 info Path attribute is absent (relying on default-path)
COOKIE_015 warning Domain=localhost
COOKIE_016 warning Domain attribute is empty
COOKIE_017 error cookie name contains reserved characters
COOKIE_018 warning cookie value is empty
COOKIE_019 warning Expires value parses but is in the past
COOKIE_020 info Max-Age is zero (cookie will be discarded immediately)
COOKIE_021 warning Path contains traversal segments / query separator
COOKIE_022 warning Domain contains a port
COOKIE_023 info RFC 2109 attribute detected ($Version, $Path, $Domain)
COOKIE_024 warning Expires uses asctime form (warning, not error)
COOKIE_PARSE error parser raised before a Cookie could be produced

Codes COOKIE_001..COOKIE_099 are part of the public contract — any change requires a major-version bump.

CLI flags

python -m cookiespec parse --kind {set-cookie,cookie} <value>
python -m cookiespec lint [--strict] [--strict-bis|--no-strict-bis] <file.har.json | ->
python -m cookiespec format <value>
python -m cookiespec --version | --help

Exit codes: 0 = success, 1 = lint found findings, 2 = invalid usage.

Examples

Parse and validate a single header

from cookiespec import parse_set_cookie, validate_set_cookie

cookie = parse_set_cookie("session=abc; Path=/; Secure; HttpOnly; SameSite=Strict")
report = validate_set_cookie(cookie)
assert report.ok

Lint a HAR file from a security audit

$ python -m cookiespec lint --strict captures/audit.har.json
3 issue(s)
  https://api.example.com/v1/login
    COOKIE_001 warning: missing Secure attribute
    COOKIE_013 error: SameSite=None requires Secure (RFC 6265bis)
  https://tracker.example.com/
    COOKIE_003 warning: missing SameSite attribute
$ echo $?
1

Default-path computation (RFC 6265 §5.1.4)

from cookiespec import default_path
default_path("https://example.com/a/b/c")  # "/a/b"
default_path("/api/v1/users")              # "/api/v1"
default_path("/")                          # "/"

Round-trip through parser/formatter

from cookiespec import parse_set_cookie, format_set_cookie
original = "session=abc; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax"
cookie = parse_set_cookie(original)
canonical = format_set_cookie(cookie)
# 'session=abc; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax'
assert parse_set_cookie(canonical).attrs == cookie.attrs

Limitations

  • The parser does not URL-decode cookie values. Per RFC 6265 §4.1.1, cookie values are raw octets. Callers that need decoding must do it themselves (most don't — the public-internet behaviour matches).
  • cookiespec parses one header value at a time. It never opens sockets, never stores cookies, never evicts them by age. Use http.cookiejar (or a real browser) for jar semantics.
  • SameSite=None requires Secure per RFC 6265bis (draft). The rule is gated behind --no-strict-bis so callers auditing legacy environments can opt out.
  • The validator's COOKIE_021 path-traversal check is a warning, not an error — Path=/../etc is technically legal per the cookie grammar but almost always a bug. Strict mode promotes it to an error.
  • Zero runtime dependencies means there is no pip install cookiespec[extras] and no Django / FastAPI integration. cookiespec is a parser, not a framework.

Non-Goals (Out of Scope)

  • Cookie-jar semantics (storage, eviction, domain-match). Use http.cookiejar for that.
  • Storing, persisting, or replaying cookies across requests. cookiespec never opens sockets.
  • RFC 2109 / RFC 2965 back-compat parsing — cookiespec is RFC 6265 only.
  • Decoding percent-encoded values — values are raw octets.
  • TLS, network IO, OAuth, JWT, or session storage.
  • JavaScript / Node port. Python-only deliverable.

Development

The specification's 700-LOC budget was a soft scope guideline, not a shipment threshold. The implementation is currently 1,662 physical lines under src/ plus 1,821 lines of tests (≈ 3,483 total) — well above the 700-line target. The variance preserves:

  • Complete type annotations and docstrings on every public symbol.
  • 121 dedicated parser tests plus 32 formatter tests, 67 validator tests, and 32 CLI tests (252 in total, every one of the 45 numbered spec acceptance criteria covered ≥1×).
  • 25 stable COOKIE_NNN validator rule codes (COOKIE_001COOKIE_024 plus COOKIE_PARSE; the public contract per the spec) instead of a hand-wavy "valid / invalid" boolean.

Trimming docstrings, types, or tests to hit the LOC target would either shrink coverage or weaken the rule-code contract. Per the repo-factory contract, clean code and complete tests take precedence over an arbitrary line count. This trade-off matches what argpeek v0.1.1 (cycle 8) and ginolint v0.1.0 (cycle 7) shipped.

License

MIT — see LICENSE.

Author: Abhishek Prasad. Cookie-jar logic not stolen from http.cookies on purpose.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cookiespec-0.1.1.tar.gz (50.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cookiespec-0.1.1-py3-none-any.whl (32.5 kB view details)

Uploaded Python 3

File details

Details for the file cookiespec-0.1.1.tar.gz.

File metadata

  • Download URL: cookiespec-0.1.1.tar.gz
  • Upload date:
  • Size: 50.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for cookiespec-0.1.1.tar.gz
Algorithm Hash digest
SHA256 0580a8a871ad03faabed6545c039e7e34a79649f19d381263bc7d00e2da836e8
MD5 b60e0518da35b12992f12865f776f8e8
BLAKE2b-256 d6bcd9e43d334e6ca30e932d5af4dc361bd7fe733d256c2ef829786f94b72f1a

See more details on using hashes here.

File details

Details for the file cookiespec-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: cookiespec-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 32.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for cookiespec-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0d29d7b064e6f86ad94c85ea39e9ab775aea92352e3173a26711a48d759698af
MD5 a8cc2c20b31a881ef9a167b0383002bf
BLAKE2b-256 ce9f89c898ee58b7353fe236e474c734034d5a7eac9ef0529cf9fb5f218c0ab4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page