Source file src/net/http/cookie.go

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

View as plain text