cookie.mx raw
1 // Copyright 2009 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package http
6
7 import (
8 "errors"
9 "fmt"
10 "internal/godebug"
11 "log"
12 "net"
13 "net/http/internal/ascii"
14 "net/textproto"
15 "strconv"
16 "bytes"
17 "time"
18 )
19
20 var httpcookiemaxnum = godebug.New("httpcookiemaxnum")
21
22 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
23 // HTTP response or the Cookie header of an HTTP request.
24 //
25 // See https://tools.ietf.org/html/rfc6265 for details.
26 type Cookie struct {
27 Name string
28 Value string
29 Quoted bool // indicates whether the Value was originally quoted
30
31 Path string // optional
32 Domain string // optional
33 Expires time.Time // optional
34 RawExpires string // for reading cookies only
35
36 // MaxAge=0 means no 'Max-Age' attribute specified.
37 // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
38 // MaxAge>0 means Max-Age attribute present and given in seconds
39 MaxAge int
40 Secure bool
41 HttpOnly bool
42 SameSite SameSite
43 Partitioned bool
44 Raw string
45 Unparsed [][]byte // Raw text of unparsed attribute-value pairs
46 }
47
48 // SameSite allows a server to define a cookie attribute making it impossible for
49 // the browser to send this cookie along with cross-site requests. The main
50 // goal is to mitigate the risk of cross-origin information leakage, and provide
51 // some protection against cross-site request forgery attacks.
52 //
53 // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
54 type SameSite int
55
56 const (
57 SameSiteDefaultMode SameSite = iota + 1
58 SameSiteLaxMode
59 SameSiteStrictMode
60 SameSiteNoneMode
61 )
62
63 var (
64 errBlankCookie = errors.New("http: blank cookie")
65 errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
66 errInvalidCookieName = errors.New("http: invalid cookie name")
67 errInvalidCookieValue = errors.New("http: invalid cookie value")
68 errCookieNumLimitExceeded = errors.New("http: number of cookies exceeded limit")
69 )
70
71 const defaultCookieMaxNum = 3000
72
73 func cookieNumWithinMax(cookieNum int) bool {
74 withinDefaultMax := cookieNum <= defaultCookieMaxNum
75 if httpcookiemaxnum.Value() == "" {
76 return withinDefaultMax
77 }
78 if customMax, err := strconv.Atoi(httpcookiemaxnum.Value()); err == nil {
79 withinCustomMax := customMax == 0 || cookieNum <= customMax
80 if withinDefaultMax != withinCustomMax {
81 httpcookiemaxnum.IncNonDefault()
82 }
83 return withinCustomMax
84 }
85 return withinDefaultMax
86 }
87
88 // ParseCookie parses a Cookie header value and returns all the cookies
89 // which were set in it. Since the same cookie name can appear multiple times
90 // the returned Values can contain more than one value for a given key.
91 func ParseCookie(line string) ([]*Cookie, error) {
92 if !cookieNumWithinMax(bytes.Count(line, ";") + 1) {
93 return nil, errCookieNumLimitExceeded
94 }
95 parts := bytes.Split(textproto.TrimString(line), ";")
96 if len(parts) == 1 && parts[0] == "" {
97 return nil, errBlankCookie
98 }
99 cookies := []*Cookie{:0:len(parts)}
100 for _, s := range parts {
101 s = textproto.TrimString(s)
102 name, value, found := bytes.Cut(s, "=")
103 if !found {
104 return nil, errEqualNotFoundInCookie
105 }
106 if !isToken(name) {
107 return nil, errInvalidCookieName
108 }
109 value, quoted, found := parseCookieValue(value, true)
110 if !found {
111 return nil, errInvalidCookieValue
112 }
113 cookies = append(cookies, &Cookie{Name: name, Value: value, Quoted: quoted})
114 }
115 return cookies, nil
116 }
117
118 // ParseSetCookie parses a Set-Cookie header value and returns a cookie.
119 // It returns an error on syntax error.
120 func ParseSetCookie(line string) (*Cookie, error) {
121 parts := bytes.Split(textproto.TrimString(line), ";")
122 if len(parts) == 1 && parts[0] == "" {
123 return nil, errBlankCookie
124 }
125 parts[0] = textproto.TrimString(parts[0])
126 name, value, ok := bytes.Cut(parts[0], "=")
127 if !ok {
128 return nil, errEqualNotFoundInCookie
129 }
130 name = textproto.TrimString(name)
131 if !isToken(name) {
132 return nil, errInvalidCookieName
133 }
134 value, quoted, ok := parseCookieValue(value, true)
135 if !ok {
136 return nil, errInvalidCookieValue
137 }
138 c := &Cookie{
139 Name: name,
140 Value: value,
141 Quoted: quoted,
142 Raw: line,
143 }
144 for i := 1; i < len(parts); i++ {
145 parts[i] = textproto.TrimString(parts[i])
146 if len(parts[i]) == 0 {
147 continue
148 }
149
150 attr, val, _ := bytes.Cut(parts[i], "=")
151 lowerAttr, isASCII := ascii.ToLower(attr)
152 if !isASCII {
153 continue
154 }
155 val, _, ok = parseCookieValue(val, false)
156 if !ok {
157 c.Unparsed = append(c.Unparsed, parts[i])
158 continue
159 }
160
161 switch lowerAttr {
162 case "samesite":
163 lowerVal, ascii := ascii.ToLower(val)
164 if !ascii {
165 c.SameSite = SameSiteDefaultMode
166 continue
167 }
168 switch lowerVal {
169 case "lax":
170 c.SameSite = SameSiteLaxMode
171 case "strict":
172 c.SameSite = SameSiteStrictMode
173 case "none":
174 c.SameSite = SameSiteNoneMode
175 default:
176 c.SameSite = SameSiteDefaultMode
177 }
178 continue
179 case "secure":
180 c.Secure = true
181 continue
182 case "httponly":
183 c.HttpOnly = true
184 continue
185 case "domain":
186 c.Domain = val
187 continue
188 case "max-age":
189 secs, err := strconv.Atoi(val)
190 if err != nil || secs != 0 && val[0] == '0' {
191 break
192 }
193 if secs <= 0 {
194 secs = -1
195 }
196 c.MaxAge = secs
197 continue
198 case "expires":
199 c.RawExpires = val
200 exptime, err := time.Parse(time.RFC1123, val)
201 if err != nil {
202 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
203 if err != nil {
204 c.Expires = time.Time{}
205 break
206 }
207 }
208 c.Expires = exptime.UTC()
209 continue
210 case "path":
211 c.Path = val
212 continue
213 case "partitioned":
214 c.Partitioned = true
215 continue
216 }
217 c.Unparsed = append(c.Unparsed, parts[i])
218 }
219 return c, nil
220 }
221
222 // readSetCookies parses all "Set-Cookie" values from
223 // the header h and returns the successfully parsed Cookies.
224 //
225 // If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
226 // GODEBUG option is not explicitly turned off, this function will silently
227 // fail and return an empty slice.
228 func readSetCookies(h Header) []*Cookie {
229 cookieCount := len(h["Set-Cookie"])
230 if cookieCount == 0 {
231 return []*Cookie{}
232 }
233 // Cookie limit was unfortunately introduced at a later point in time.
234 // As such, we can only fail by returning an empty slice rather than
235 // explicit error.
236 if !cookieNumWithinMax(cookieCount) {
237 return []*Cookie{}
238 }
239 cookies := []*Cookie{:0:cookieCount}
240 for _, line := range h["Set-Cookie"] {
241 if cookie, err := ParseSetCookie(line); err == nil {
242 cookies = append(cookies, cookie)
243 }
244 }
245 return cookies
246 }
247
248 // SetCookie adds a Set-Cookie header to the provided [ResponseWriter]'s headers.
249 // The provided cookie must have a valid Name. Invalid cookies may be
250 // silently dropped.
251 func SetCookie(w ResponseWriter, cookie *Cookie) {
252 if v := cookie.String(); v != "" {
253 w.Header().Add("Set-Cookie", v)
254 }
255 }
256
257 // String returns the serialization of the cookie for use in a [Cookie]
258 // header (if only Name and Value are set) or a Set-Cookie response
259 // header (if other fields are set).
260 // If c is nil or c.Name is invalid, the empty string is returned.
261 func (c *Cookie) String() string {
262 if c == nil || !isToken(c.Name) {
263 return ""
264 }
265 // extraCookieLength derived from typical length of cookie attributes
266 // see RFC 6265 Sec 4.1.
267 const extraCookieLength = 110
268 var b bytes.Buffer
269 b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength)
270 b.WriteString(c.Name)
271 b.WriteRune('=')
272 b.WriteString(sanitizeCookieValue(c.Value, c.Quoted))
273
274 if len(c.Path) > 0 {
275 b.WriteString("; Path=")
276 b.WriteString(sanitizeCookiePath(c.Path))
277 }
278 if len(c.Domain) > 0 {
279 if validCookieDomain(c.Domain) {
280 // A c.Domain containing illegal characters is not
281 // sanitized but simply dropped which turns the cookie
282 // into a host-only cookie. A leading dot is okay
283 // but won't be sent.
284 d := c.Domain
285 if d[0] == '.' {
286 d = d[1:]
287 }
288 b.WriteString("; Domain=")
289 b.WriteString(d)
290 } else {
291 log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain)
292 }
293 }
294 var buf [29]byte // len(TimeFormat)
295 if validCookieExpires(c.Expires) {
296 b.WriteString("; Expires=")
297 b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat))
298 }
299 if c.MaxAge > 0 {
300 b.WriteString("; Max-Age=")
301 b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10))
302 } else if c.MaxAge < 0 {
303 b.WriteString("; Max-Age=0")
304 }
305 if c.HttpOnly {
306 b.WriteString("; HttpOnly")
307 }
308 if c.Secure {
309 b.WriteString("; Secure")
310 }
311 switch c.SameSite {
312 case SameSiteDefaultMode:
313 // Skip, default mode is obtained by not emitting the attribute.
314 case SameSiteNoneMode:
315 b.WriteString("; SameSite=None")
316 case SameSiteLaxMode:
317 b.WriteString("; SameSite=Lax")
318 case SameSiteStrictMode:
319 b.WriteString("; SameSite=Strict")
320 }
321 if c.Partitioned {
322 b.WriteString("; Partitioned")
323 }
324 return b.String()
325 }
326
327 // Valid reports whether the cookie is valid.
328 func (c *Cookie) Valid() error {
329 if c == nil {
330 return errors.New("http: nil Cookie")
331 }
332 if !isToken(c.Name) {
333 return errors.New("http: invalid Cookie.Name")
334 }
335 if !c.Expires.IsZero() && !validCookieExpires(c.Expires) {
336 return errors.New("http: invalid Cookie.Expires")
337 }
338 for i := 0; i < len(c.Value); i++ {
339 if !validCookieValueByte(c.Value[i]) {
340 return fmt.Errorf("http: invalid byte %q in Cookie.Value", c.Value[i])
341 }
342 }
343 if len(c.Path) > 0 {
344 for i := 0; i < len(c.Path); i++ {
345 if !validCookiePathByte(c.Path[i]) {
346 return fmt.Errorf("http: invalid byte %q in Cookie.Path", c.Path[i])
347 }
348 }
349 }
350 if len(c.Domain) > 0 {
351 if !validCookieDomain(c.Domain) {
352 return errors.New("http: invalid Cookie.Domain")
353 }
354 }
355 if c.Partitioned {
356 if !c.Secure {
357 return errors.New("http: partitioned cookies must be set with Secure")
358 }
359 }
360 return nil
361 }
362
363 // readCookies parses all "Cookie" values from the header h and
364 // returns the successfully parsed Cookies.
365 //
366 // If filter isn't empty, only cookies of that name are returned.
367 //
368 // If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
369 // GODEBUG option is not explicitly turned off, this function will silently
370 // fail and return an empty slice.
371 func readCookies(h Header, filter string) []*Cookie {
372 lines := h["Cookie"]
373 if len(lines) == 0 {
374 return []*Cookie{}
375 }
376
377 // Cookie limit was unfortunately introduced at a later point in time.
378 // As such, we can only fail by returning an empty slice rather than
379 // explicit error.
380 cookieCount := 0
381 for _, line := range lines {
382 cookieCount += bytes.Count(line, ";") + 1
383 }
384 if !cookieNumWithinMax(cookieCount) {
385 return []*Cookie{}
386 }
387
388 cookies := []*Cookie{:0:len(lines)+bytes.Count(lines[0], ";")}
389 for _, line := range lines {
390 line = textproto.TrimString(line)
391
392 var part string
393 for len(line) > 0 { // continue since we have rest
394 part, line, _ = bytes.Cut(line, ";")
395 part = textproto.TrimString(part)
396 if part == "" {
397 continue
398 }
399 name, val, _ := bytes.Cut(part, "=")
400 name = textproto.TrimString(name)
401 if !isToken(name) {
402 continue
403 }
404 if filter != "" && filter != name {
405 continue
406 }
407 val, quoted, ok := parseCookieValue(val, true)
408 if !ok {
409 continue
410 }
411 cookies = append(cookies, &Cookie{Name: name, Value: val, Quoted: quoted})
412 }
413 }
414 return cookies
415 }
416
417 // validCookieDomain reports whether v is a valid cookie domain-value.
418 func validCookieDomain(v string) bool {
419 if isCookieDomainName(v) {
420 return true
421 }
422 if net.ParseIP(v) != nil && !bytes.Contains(v, ":") {
423 return true
424 }
425 return false
426 }
427
428 // validCookieExpires reports whether v is a valid cookie expires-value.
429 func validCookieExpires(t time.Time) bool {
430 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601
431 return t.Year() >= 1601
432 }
433
434 // isCookieDomainName reports whether s is a valid domain name or a valid
435 // domain name with a leading dot '.'. It is almost a direct copy of
436 // package net's isDomainName.
437 func isCookieDomainName(s string) bool {
438 if len(s) == 0 {
439 return false
440 }
441 if len(s) > 255 {
442 return false
443 }
444
445 if s[0] == '.' {
446 // A cookie a domain attribute may start with a leading dot.
447 s = s[1:]
448 }
449 last := byte('.')
450 ok := false // Ok once we've seen a letter.
451 partlen := 0
452 for i := 0; i < len(s); i++ {
453 c := s[i]
454 switch {
455 default:
456 return false
457 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
458 // No '_' allowed here (in contrast to package net).
459 ok = true
460 partlen++
461 case '0' <= c && c <= '9':
462 // fine
463 partlen++
464 case c == '-':
465 // Byte before dash cannot be dot.
466 if last == '.' {
467 return false
468 }
469 partlen++
470 case c == '.':
471 // Byte before dot cannot be dot, dash.
472 if last == '.' || last == '-' {
473 return false
474 }
475 if partlen > 63 || partlen == 0 {
476 return false
477 }
478 partlen = 0
479 }
480 last = c
481 }
482 if last == '-' || partlen > 63 {
483 return false
484 }
485
486 return ok
487 }
488
489 var cookieNameSanitizer = bytes.NewReplacer("\n", "-", "\r", "-")
490
491 func sanitizeCookieName(n string) string {
492 return cookieNameSanitizer.Replace(n)
493 }
494
495 // sanitizeCookieValue produces a suitable cookie-value from v.
496 // It receives a quoted bool indicating whether the value was originally
497 // quoted.
498 // https://tools.ietf.org/html/rfc6265#section-4.1.1
499 //
500 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
501 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
502 // ; US-ASCII characters excluding CTLs,
503 // ; whitespace DQUOTE, comma, semicolon,
504 // ; and backslash
505 //
506 // We loosen this as spaces and commas are common in cookie values
507 // thus we produce a quoted cookie-value if v contains commas or spaces.
508 // See https://golang.org/issue/7243 for the discussion.
509 func sanitizeCookieValue(v string, quoted bool) string {
510 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
511 if len(v) == 0 {
512 return v
513 }
514 if bytes.ContainsAny(v, " ,") || quoted {
515 return `"` + v + `"`
516 }
517 return v
518 }
519
520 func validCookieValueByte(b byte) bool {
521 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
522 }
523
524 // path-av = "Path=" path-value
525 // path-value = <any CHAR except CTLs or ";">
526 func sanitizeCookiePath(v string) string {
527 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
528 }
529
530 func validCookiePathByte(b byte) bool {
531 return 0x20 <= b && b < 0x7f && b != ';'
532 }
533
534 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
535 ok := true
536 for i := 0; i < len(v); i++ {
537 if valid(v[i]) {
538 continue
539 }
540 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
541 ok = false
542 break
543 }
544 if ok {
545 return v
546 }
547 buf := []byte{:0:len(v)}
548 for i := 0; i < len(v); i++ {
549 if b := v[i]; valid(b) {
550 buf = append(buf, b)
551 }
552 }
553 return string(buf)
554 }
555
556 // parseCookieValue parses a cookie value according to RFC 6265.
557 // If allowDoubleQuote is true, parseCookieValue will consider that it
558 // is parsing the cookie-value;
559 // otherwise, it will consider that it is parsing a cookie-av value
560 // (cookie attribute-value).
561 //
562 // It returns the parsed cookie value, a boolean indicating whether the
563 // parsing was successful, and a boolean indicating whether the parsed
564 // value was enclosed in double quotes.
565 func parseCookieValue(raw string, allowDoubleQuote bool) (value string, quoted, ok bool) {
566 // Strip the quotes, if present.
567 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
568 raw = raw[1 : len(raw)-1]
569 quoted = true
570 }
571 for i := 0; i < len(raw); i++ {
572 if !validCookieValueByte(raw[i]) {
573 return "", quoted, false
574 }
575 }
576 return raw, quoted, true
577 }
578