The universal cookie npm package is a small, actively maintained library for reading and writing cookies with one API on both the server and the browser — and like most cookie libraries, the security of your application depends far less on the package than on the cookie attributes you set with it. This review covers what universal-cookie does, the notable advisory it inherited through its cookie dependency, and the security fundamentals every cookie you write should carry.
What universal-cookie is for
Isomorphic and server-rendered apps have a cookie problem: on the client, cookies live in document.cookie; on the server, they arrive in a request header and leave in a Set-Cookie response header. universal-cookie (from the reactivestack/cookies monorepo, which also ships react-cookie) papers over that difference:
import Cookies from "universal-cookie";
// browser: reads document.cookie
const cookies = new Cookies(null, { path: "/" });
// server (Express): hydrate from the request
const cookies = new Cookies(req.headers.cookie, { path: "/" });
cookies.set("theme", "dark", { path: "/", maxAge: 60 * 60 * 24 * 365 });
const theme = cookies.get("theme");
The same component or utility can call cookies.get() during server-side rendering and in the hydrated client, which is the entire pitch. react-cookie layers a provider and hooks on top for React apps; universal-cookie itself is framework-agnostic. Under the hood, parsing and serialization are delegated to the well-known cookie package — a design decision that matters for the security story below.
Package health at a glance
By standard review criteria, npm universal-cookie is in good shape: it is actively released, has a large install base via react-cookie and direct use, keeps a tiny dependency footprint (essentially just cookie), and has no direct CVEs recorded against it in the public advisory databases. The recent major lines are TypeScript-typed and the API has been stable for years. The bus-factor and cadence profile is typical of a mature utility: quiet, boring, and mostly maintenance releases — which is what you want from a cookie wrapper.
The one blemish worth knowing about arrived through the dependency, not the package.
The inherited advisory: CVE-2024-47764 in cookie
In October 2024, the cookie package — the parser/serializer universal-cookie builds on — received CVE-2024-47764. Versions before cookie 0.7.0 validated cookie names, paths, and domains too permissively, so a name containing separator characters could smuggle extra attributes into the serialized Set-Cookie string. The advisory's example: serializing a cookie whose name is userName=<script>alert('XSS3')</script>; Max-Age=2592000; a results in output that sets userName to an unexpected value and injects attacker-chosen fields.
The practical precondition is that your code passes user-controlled input as a cookie name, path, or domain — an unusual but not unheard-of pattern (think per-user or per-tenant cookie names built from input). If you never do that, exploitability drops to near zero; if you do, the bug allowed cookie injection with XSS-adjacent consequences. The fix in cookie 0.7.0 tightened validation to RFC 6265's grammar.
What this means for universal-cookie users: current releases resolve to a patched cookie (the latest major depends on the modern cookie 1.x line), but an old lockfile can happily keep resolving cookie below 0.7.0 for years. Check with:
npm ls cookie
and confirm every resolved instance is at 0.7.0 or later. This is the textbook transitive-dependency case — the package you installed was never vulnerable, the one it delegates to was — and it is why SCA scanning works at the resolved-lockfile level rather than the package.json level. A scanner such as Safeguard flags the vulnerable cookie resolution and points at universal-cookie (or whatever else pinned it) as the upgrade lever.
The security basics: attributes beat libraries
However you write cookies, four attributes decide most of your cookie security posture.
HttpOnly — the big one, and the one a client-side library cannot give you. An HttpOnly cookie is invisible to JavaScript, including universal-cookie, which means XSS cannot exfiltrate it. Session identifiers and auth tokens should be HttpOnly, set by the server via Set-Cookie. A corollary that surprises people: if your session cookie is readable by universal-cookie in the browser, that is a finding, not a feature.
Secure — the cookie only travels over HTTPS. There is no reason to omit this in 2025; set it on everything.
SameSite — controls cross-site sending and is your ambient CSRF defense. Lax (the modern browser default) is right for most session cookies; Strict for high-sensitivity actions; None only for deliberate cross-site embedding, and it requires Secure.
Scope: Domain, Path, and prefixes — the narrowest scope that works. Omitting Domain binds the cookie to the exact host, which is usually what you want. The __Host- name prefix enforces host-locked, secure, path-/ cookies at the browser level:
cookies.set("__Host-csrf", token, {
path: "/",
secure: true,
sameSite: "strict",
});
With universal-cookie all of these are options on set() — the library will faithfully write whatever you ask, including insecure combinations, so the discipline lives in your code review checklist. Cookie flags are also one of the easiest things for a DAST scan to verify from the outside: a scanner sees every Set-Cookie your app emits in real responses and flags missing HttpOnly, Secure, or SameSite without needing source access.
Patterns to avoid with client-writable cookies
Two anti-patterns account for most cookie findings in universal cookie codebases:
- JWTs in JS-readable cookies. Storing an access token in a non-HttpOnly cookie combines the worst of both worlds: XSS can steal it (like localStorage) and it is sent automatically cross-request (CSRF exposure). If the server needs it, the server should set it HttpOnly; if only the SPA needs it, keep it in memory.
- Trusting cookie values as data. Anything readable and writable from the client is attacker input. A
role=admincookie checked on the server without a signature is an authorization bypass. Client-set cookies are fine for preferences (theme, locale, dismissed banners) — the stakes where tampering does not matter.
Verdict
universal-cookie earns an easy recommendation for its intended job: preference-grade cookie handling across server and client in isomorphic apps. Keep it current so the cookie dependency stays patched, keep secrets in server-set HttpOnly cookies where the library (by design) cannot touch them, and let your scanners verify the flags in production responses. The library is fine; the attributes are the security control.
FAQ
Is the universal-cookie npm package safe?
Yes. It has no direct CVEs; the notable issue was CVE-2024-47764 in its cookie dependency (fixed in cookie 0.7.0), inherited by old lockfile resolutions. Run npm ls cookie and upgrade if anything resolves below 0.7.0.
What is the difference between universal-cookie and react-cookie?
They ship from the same monorepo. universal-cookie is the framework-agnostic core with a Cookies class for get/set/remove on server and client; react-cookie wraps it with a React provider and hooks (useCookies) for component-friendly access.
Can universal-cookie read HttpOnly cookies?
No — in the browser, no JavaScript can, which is the point of HttpOnly. On the server you can hydrate universal-cookie from the raw Cookie request header, where HttpOnly cookies are visible because the flag only restricts browser-side script access.
Should I store a session token with universal-cookie?
No. Session identifiers and auth tokens should be set by the server as HttpOnly; Secure; SameSite cookies so XSS cannot read them. Use universal-cookie for non-sensitive state like themes, locale, and UI preferences.