A cookie’s host and path scope controls which destinations are eligible to receive its name and value. It is useful to minimize that set, but scope is not authorization, and cookies do not follow the browser’s full origin boundary. This article concentrates on Domain and Path; transport and request context add separate restrictions.

A host matrix compares omitted Domain with parent Domain. Two setting paths show default-Path derivation; a separate Path=/admin example shows the slash boundary. These are eligibility checks, subject to other cookie restrictions.
These are eligibility checks, not authorization or guaranteed delivery. Public-suffix and other cookie rules still apply. Paths and ports do not isolate untrusted applications; this is not browser-conformance evidence.

Domain chooses hosts, not ports

Omitting Domain creates a host-only cookie. A cookie set by app.example.com then matches that host, not shop.example.com. Adding Domain=example.com allows the parent and matching subdomains, subject to other cookie restrictions. A leading dot is ignored during attribute processing; it does not create a separate wildcard feature. Keep scope narrow unless the wider recipients genuinely need the value.

A setting host cannot nominate an unrelated sibling: app.example.com cannot set Domain=shop.example.com. Browsers also apply public-suffix restrictions, so simple string suffix matching is not a complete cookie implementation. The model below uses only known example hostnames and deliberately leaves public-suffix lookup, internationalized names, and IP-address handling outside its scope. Do not copy it into an authentication library.

Ports are not a cookie boundary. An eligible cookie for app.example.com can be sent to another port on that host, even though changing the port changes the web origin. A service on an unexpected port therefore belongs in the trust assessment. Separating untrusted applications by path or port does not provide the isolation of genuinely separate cookie hosts.

Path defaults come from the setting request

When Path is omitted or invalid, the browser computes a default from the setting request’s path. For /account/login, that default is /account, not /. For /login, it is /. Moving a login endpoint can therefore change an implicitly scoped cookie without changing its Set-Cookie string. Set Path explicitly when the application requires a stable scope.

Matching also respects a slash boundary: /admin matches itself and /admin/users, but not /administrator. It is not a filesystem permission. Same-origin script can interact with other paths, and Path must not be used to isolate mutually untrusted applications. A narrower path reduces eligible requests; it does not replace server-side checks or prevent every way of reading the value.

When it breaks

A parent-domain session reaches a forgotten sibling service that logs incoming cookies. HttpOnly does not protect against the receiving server reading its own request headers. Conversely, an unexpectedly narrow default path causes a successful login to appear lost on another route. Diagnose both cases using the setting URL and attributes together, without printing a real session token into logs or support tickets.

Overlapping cookies with the same name can also coexist at different scopes. The Cookie request header contains name/value pairs, not their Domain or Path attributes, so the application cannot recover scope from that header alone. Avoid ambiguous duplicate session names and expire an old cookie with matching scope when migrating it. Deleting a different path’s cookie does not remove the original.

A supported __Host- name requires a secure setting context, Secure, explicit Path=/, and no Domain. This prevents a sibling from planting a parent-domain cookie under that prefixed name. It still does not isolate ports or fix a compromised setting host. Regenerate session identifiers after authentication as well; prefixes alone do not solve session fixation.

Check it quickly

Save this as cookie-scope.py and run python3 cookie-scope.py with Python 3.11 or newer. It opens no sockets and uses synthetic URLs. These assertions test a deliberately limited domain/path model, not browser storage, SameSite, expiry, prefixes, or third-party cookie policy.

from urllib.parse import urlsplit


def default_path(request_path):
    if not request_path.startswith("/") or request_path.count("/") <= 1:
        return "/"
    return request_path.rsplit("/", 1)[0]


def domain_match(host, domain):
    return host == domain or host.endswith("." + domain)


def path_match(request_path, cookie_path):
    return request_path == cookie_path or (
        request_path.startswith(cookie_path)
        and (cookie_path.endswith("/")
             or request_path[len(cookie_path):].startswith("/"))
    )


def scoped_cookie(setting_url, domain=None, path=None):
    setting = urlsplit(setting_url)
    host = setting.hostname
    selected = host if domain is None else domain.removeprefix(".").lower()
    if domain is not None and not domain_match(host, selected):
        raise ValueError("Domain does not match the setting host")
    return selected, domain is None, (
        path if path and path.startswith("/") else default_path(setting.path)
    )


def matches(cookie, request_url):
    domain, host_only, path = cookie
    request = urlsplit(request_url)
    host_ok = (request.hostname == domain if host_only
               else domain_match(request.hostname, domain))
    return host_ok and path_match(request.path or "/", path)


setting = "https://app.example.com/account/login"
host_only = scoped_cookie(setting)
broad = scoped_cookie(setting, domain=".example.com", path="/")
assert host_only == ("app.example.com", True, "/account")
assert broad == scoped_cookie(setting, domain="example.com", path="/")
assert matches(host_only, "https://app.example.com/account/profile")
assert not matches(host_only, "https://app.example.com/dashboard")
assert not matches(host_only, "https://shop.example.com/account/profile")
assert matches(broad, "https://shop.example.com/")
assert not matches(broad, "https://example.com.attacker.invalid/")
assert matches(host_only, "https://app.example.com:8443/account/profile")
for path, expected in [("/admin", True), ("/admin/", True),
                       ("/admin/users", True), ("/adminx", False),
                       ("/administrator", False)]:
    assert path_match(path, "/admin") == expected
try:
    scoped_cookie(setting, domain="shop.example.com")
except ValueError:
    pass
else:
    raise AssertionError("unrelated Domain accepted")
print("PASS: default path, host-only, domain, path boundary, and port cases")

The success line confirms the request-derived /account default, host-only exclusion of a sibling, broader parent-domain matching, slash-sensitive paths, and port independence. A sibling Domain assignment raises ValueError. These are explicit cases rather than a claim of browser conformance.

For deployment validation, inspect sanitized cookie attributes in browser developer tools and test the intended route matrix with a synthetic account. Check both the cookie store and outgoing requests: a missing header can reflect rejection, expiry, scope, or another browser policy. Never send a production token to an untrusted host merely to demonstrate that its scope is too broad.

Sources