RFC 6265 Set-Cookie / Cookie header parser, validator, and linter for Python.
Project description
cookiespec
"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 |
http.cookiessilently swallows cookies with control characters (CPython #61542) — the cookie parses successfully but the dict is empty.http.cookiesdoes not parse RFC 850 or asctimeExpiresformats (psf/requests #6004) — the cookie parses but theExpiresattribute 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).
cookiespecparses one header value at a time. It never opens sockets, never stores cookies, never evicts them by age. Usehttp.cookiejar(or a real browser) for jar semantics.SameSite=NonerequiresSecureper RFC 6265bis (draft). The rule is gated behind--no-strict-bisso callers auditing legacy environments can opt out.- The validator's
COOKIE_021path-traversal check is a warning, not an error —Path=/../etcis 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.cookiespecis a parser, not a framework.
Non-Goals (Out of Scope)
- Cookie-jar semantics (storage, eviction, domain-match). Use
http.cookiejarfor that. - Storing, persisting, or replaying cookies across requests.
cookiespecnever opens sockets. - RFC 2109 / RFC 2965 back-compat parsing —
cookiespecis 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_NNNvalidator rule codes (COOKIE_001…COOKIE_024plusCOOKIE_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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters