仓库源文站点原文


layout: "../layouts/BlogPost.astro" title: "HTTP cookies explained and security policy" slug: http-cookies-explained-and-security description: "" added: "Nov 16 2022" tags: [web]

updatedDate: "Oct 17 2024"

An HTTP cookie is a small piece of data that a server sends to the user's web browser. The browser may store it and send it back with later requests to the same server. Typically, it's used to tell if two requests came from the same browser — keeping a user logged-in, for example. It remembers stateful information for the stateless HTTP protocol.

After receiving an HTTP request, a server can send one or more Set-Cookie headers with the response. The cookie is stored by the browser, and then the cookie is sent with requests inside a Cookie HTTP header. An expiration date or duration can be specified, after which the cookie is no longer sent. Additional restrictions to a specific domain and path can be set, limiting where the cookie is sent.

HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

GET /sample_page.html HTTP/2.0
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

Basic HTTP cookie parser and serializer for HTTP servers: https://github.com/jshttp/cookie

// Set a new cookie with the name: cookie.serialize(name, value, options)
res.setHeader(
  "Set-Cookie",
  cookie.serialize("name", String(query.name), {
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 7, // 1 week
  }),
);

// Parse the cookies on the request: cookie.parse(str, options)
var cookies = cookie.parse(req.headers.cookie || "");

Cookie restrictions

Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<number>; Domain=<domain-value>; Secure; HttpOnly

Set-Cookie: sessionId=38afes7a8
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT
Set-Cookie: mykey=myvalue; SameSite=Strict

The lifetime of a cookie can be defined in two ways:

A cookie with the Secure attribute is sent to the server only over the HTTPS protocol, never with unsecured HTTP (except on localhost). Insecure sites http: cannot set cookies with the Secure attribute. A cookie with the HttpOnly attribute is inaccessible to the JavaScript document.cookie API; it is sent only to the server.

Normally localhost should be treated as a secure origin even if not HTTPs. Both Chrome (https://bugs.chromium.org/p/chromium/issues/detail?id=1056543) and Firefox (https://bugzilla.mozilla.org/show_bug.cgi?id=1648993) fixed issues to allow Secure cookies for localhost. The https: requirements are ignored when the Secure attribute is set by localhost since Firefox 75 and Chrome 89. However, Safari doesn’t set Secure cookies on localhost, as Firefox and Chrome do.

The Domain attribute specifies which hosts are allowed to receive the cookie. If unspecified, defaults to the host of the current document URL, not including subdomains. If Domain is specified, then subdomains are always included. For example, if the value of the Domain attribute is "example.com", the user agent will include the cookie in the Cookie header when making HTTP requests to example.com, www.example.com, and www.corp.example.com. If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.

The Path attribute indicates a URL path that must exist in the requested URL in order to send the Cookie. For example, if Path=/docs is set, /docs, /docs/Web/, /docs/Web/HTTP are all matched.

The SameSite attribute lets servers require that a cookie shouldn't be sent with cross-origin requests. It takes three possible values: Strict, Lax, and None. With Strict, the cookie is sent only to the same site as the one that originated it. In user terms, the cookie will only be sent if the site for the cookie matches the site currently shown in the browser's URL bar. Lax is similar, with an exception for when the user navigates to a URL from an external site, such as by following a link. This is the default behavior if the SameSite attribute is not specified. None has no restrictions on cross-site requests, but requires that the Secure attribute must be used: SameSite=None; Secure.

SameSite=Lax cookies are not sent:

Cross-Site Request Forgery (CSRF) attacks

These attacks are possible because web browsers send authentication tokens automatically with every request to the server. It takes advantage of the user's previously authenticated session. An example of a CSRF attack:

  1. A user signs into www.good-banking-site.com. The server authenticates the user and issues a response that includes an authentication cookie. The site is vulnerable to attack because it trusts any request that it receives with a valid authentication cookie.
  2. The user visits a malicious site www.bad-crook-site.com. It contains an HTML form similar to the following:
    <h1>Congratulations! You're a Winner!</h1>
    <form action="http://good-banking-site.com/api/account" method="post">
     <input type="hidden" name="Transaction" value="withdraw">
     <input type="hidden" name="Amount" value="1000000">
     <input type="submit" value="Click to collect your prize!">
    </form>
    
  3. The user clicks the submit button. The browser makes the request and automatically includes the authentication cookie for the requested domain www.good-banking-site.com. The server has the user's authentication context and can perform any action that an authenticated user is allowed to perform.

How to prevent CSRF:

Cross-site scripting

Cross-site scripting (XSS) is a security bug that can affect websites. This bug can allow an attacker to add their own malicious JavaScript code onto the HTML pages displayed to the users. The vulnerabilities most often happen when user input is sent to the server, and the server responds back to the user by displaying a page that includes the user input without validation. XSS also can occur entirely in the client-side without data being sent back and forth between the client and server.

A common technique for preventing XSS vulnerabilities is "escaping". The purpose of character and string escaping is to make sure that every part of a string is interpreted as a string primitive, not as a control character or code. Escape certain characters (like <, >, &, and ") with HTML entity to prevent them being executed.

A good test string is >'>"><img src=x onerror=alert(0)>. If your application doesn't correctly escape this string, you will see an alert and will know that something went wrong. The Big List of Naughty Strings is a list of strings which have a high probability of causing issues when used as user-input data.

We do not recommend that you manually escape user-supplied data. Instead, we strongly recommend that you use a templating system or web development framework that provides context-aware auto-escaping. If this is impossible for your website, use existing libraries (e.g., DOMPurify, escape-html) that are known to work, and apply them consistently to all user-supplied data.

For example in Vue, whether using templates or render functions, content is automatically escaped. vue-dompurify-html is a "safe" replacement for the v-html directive. The HTML code is sanitized with DOMPurify before being interpreted.

It’s recommended to avoid storing any sensitive information in local storage where authentication would be assumed. You can trivially read all data stored in local storage with Object.entries(localStorage). This means if your website is vulnerable to XSS attacks, where a third party can run arbitrary scripts, your users’ tokens can be easily stolen. Cookies, on the other hand, can’t be read by client-side JS if you add the HttpOnly flag.

Content Security Policy

Configuring Content Security Policy involves adding the Content-Security-Policy HTTP header to a web page and giving it values to control what resources the user agent is allowed to load for that page. If the site doesn't offer the CSP header, browsers likewise use the standard same-origin policy. A properly designed Content Security Policy helps protect a page against a cross-site scripting attack. There are specific directives for a wide variety of types of items, so that each type can have its own policy, including fonts, frames, images, audio and video media, scripts, and workers.

Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; img-src 'self' img.example.com; style-src 'self';

The above policy permits:


iframe 跨域嵌入的实践

网站 A 嵌入网站 B,且 A 和 B 是不同域名时,主要需要解决跨域请求 CORS 和 Cookie 的问题。先说预期,我们希望的是 A 网站嵌入 B 网站的同时,B 网站可以正常请求 B 域名的接口和发送 B 域名下的 Cookie。

CORS 的处理

先说跨域请求,由于我们的请求目标应该都是 B,所以不涉及跨域。但如果出现在 A 域名中请求 B,需要 B 域名的服务端设置 Access-Control-Allow-Origin: 'https://a-domain.com'Access-Control-Allow-Credentials: true,后者允许浏览器在跨域请求中发送 Cookies。注意此时 Access-Control-Allow-Origin 不能使用通配符 *,否则无法携带 Cookie 等凭证。

Cookie 的处理

对于 Cookie 的问题,首先浏览器默认会给第三方 Cookie 添加 SameSite=Lax 属性,意味着 A 域名跨域请求 B 域名,不会携带 Cookie。所以需要 B 域名手动设置 Cookie SameSite=None; Secure 属性,确保 Cookie 可以在跨域上下文中发送 (此时也增加了 CSRF 攻击的风险,建议使用 CSRF Token)

上述方案基本可行,但是有一个长期维护的问题。Chrome 118 开始有第三方 Cookie 的警告,2024 年 Q1-Q3 逐步禁用 SameSite=None; Secure,请求时无法读取并携带第三方 Cookie。浏览器也有设置 flag 可以开启进行实验:

<img alt="third-party cookie" src="https://raw.githubusercontent.com/kexiZeroing/blog-images/main/Test%20Third%20Party%20Cookie%20Phaseout.png" width="500">

从长期考虑,可以使用 Partitioned Cookies 方案(这也官方推荐的方式,别名叫做 CHIPS,即 Cookies Having Independent Partitioned State)。大概意思是如果 A 要嵌入 C,C 在它的 Cookie 上指定了 partitioned 属性,这个 Cookie 将保存在一个特殊的分区 jar 中。 它只会在 A 中通过 iframe 嵌入 C 时才会生效,浏览器会判断顶级网站为 A 时才发送该 Cookie。如果 B 也通过 iframe 嵌入了 C,这时在 B 下的 C 是无法访问到之前在 A 下面设置的那个 Cookie 的。如果用户直接访问 C,一样也是访问不到这个 Cookie 的。

关于 Partitioned Cookie 的参考链接:

其实还有另一种方式避开 Cookie 的限制,就是使用 JWT 代替做验证,在域名 A 下生成 token,并传递给 iframe(url 参数或 postMessage)。后续 B 域名通过 JWT 验证请求中的用户身份。前端可以在所有发往 B 的请求上添加 Authorization 请求头,但是服务端 B 的认证方式都要修改。所以说 JWT 能避免浏览器的第三方 Cookie 限制,但也需要额外的 token 管理机制和服务端验证更新策略。

结论和方案

三种可能的模式如下,无论哪种都涉及跨域,都要按上面描述的方式设置 Cookie,后两种还有一些额外的工作。