def test_csrf_middleware(cookie_value, header_value, should_return_error): req, resp, resource, params = Mock(), Mock(), Mock(), Mock() req.cookies = { settings.API_CSRF_COOKIE_NAME: cookie_value, } req.headers = { settings.API_CSRF_HEADER_NAME: header_value, } req.method = "POST" # testing CSRF -- need to be unsafe method resp.cookies = {} resp.headers = {} resp.complete = False resp.status = GOOD_STATUS resp.text = GOOD_BODY resource.csrf_exempt = False middleware = CsrfMiddleware() middleware.default_secret = DEFAULT_SESSION_SECRET middleware.process_resource(req, resp, resource, params) if should_return_error: assert resp.status == BAD_STATUS, "response status should be %s, is %s" % ( BAD_STATUS, resp.status) assert resp.text == BAD_BODY, "response body should be a proper error message" assert resp.complete, "response should be complete in case of error" else: assert resp.status == GOOD_STATUS assert resp.text == GOOD_BODY assert not resp.complete middleware.process_response(req, resp, None, None) resp.append_header.assert_called() cookie_domains = set() for call_args, call_kwargs in resp.append_header.call_args_list: assert call_args[0] == "Set-Cookie" cookies = http_cookies.SimpleCookie(call_args[1]) assert cookies.get('mcod_csrf_token', None) is not None cookie = cookies['mcod_csrf_token'] new_cookie_value = cookie.value assert new_cookie_value != cookie_value, "server should assign a new CSRF token for every response" assert cookie["path"] == '/' assert ((not settings.SESSION_COOKIE_SECURE and not cookie["secure"]) or (settings.SESSION_COOKIE_SECURE and cookie["secure"])) cookie_domain = cookie["domain"] assert cookie_domain in settings.API_CSRF_COOKIE_DOMAINS cookie_domains.add(cookie_domain) assert not cookie["httponly"] assert cookie_domains == set(settings.API_CSRF_COOKIE_DOMAINS)
def __init__(self, iterable, status, headers): self._text = None self._content = b''.join(iterable) self._status = status self._status_code = int(status[:3]) self._headers = CaseInsensitiveDict(headers) cookies = http_cookies.SimpleCookie() for name, value in headers: if name.lower() == 'set-cookie': cookies.load(value) self._cookies = dict( (morsel.key, Cookie(morsel)) for morsel in cookies.values()) self._encoding = helpers.get_encoding_from_headers(self._headers)
def unset_cookie(self, name): """Unset a cookie in the response Clears the contents of the cookie, and instructs the user agent to immediately expire its own copy of the cookie. Warning: In order to successfully remove a cookie, both the path and the domain must match the values that were used when the cookie was created. """ if self._cookies is None: self._cookies = http_cookies.SimpleCookie() self._cookies[name] = '' # NOTE(Freezerburn): SimpleCookie apparently special cases the # expires attribute to automatically use strftime and set the # time as a delta from the current time. We use -1 here to # basically tell the browser to immediately expire the cookie, # thus removing it from future request objects. self._cookies[name]['expires'] = -1
def unset_cookie(self, name, domain=None, path=None): """Unset a cookie in the response. Clears the contents of the cookie, and instructs the user agent to immediately expire its own copy of the cookie. Note: Modern browsers place restriction on cookies without the "same-site" cookie attribute set. To that end this attribute is set to ``'Lax'`` by this method. (See also: `Same-Site warnings`_) Warning: In order to successfully remove a cookie, both the path and the domain must match the values that were used when the cookie was created. Args: name (str): Cookie name Keyword Args: domain (str): Restricts the cookie to a specific domain and any subdomains of that domain. By default, the user agent will return the cookie only to the origin server. When overriding this default behavior, the specified domain must include the origin server. Otherwise, the user agent will reject the cookie. Note: Cookies do not provide isolation by port, so the domain should not provide one. (See also: RFC 6265, Section 8.5) (See also: RFC 6265, Section 4.1.2.3) path (str): Scopes the cookie to the given path plus any subdirectories under that path (the "/" character is interpreted as a directory separator). If the cookie does not specify a path, the user agent defaults to the path component of the requested URI. Warning: User agent interfaces do not always isolate cookies by path, and so this should not be considered an effective security measure. (See also: RFC 6265, Section 4.1.2.4) .. _Same-Site warnings: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings """ # noqa: E501 if self._cookies is None: self._cookies = http_cookies.SimpleCookie() self._cookies[name] = '' # NOTE(Freezerburn): SimpleCookie apparently special cases the # expires attribute to automatically use strftime and set the # time as a delta from the current time. We use -1 here to # basically tell the browser to immediately expire the cookie, # thus removing it from future request objects. self._cookies[name]['expires'] = -1 # NOTE(CaselIT): Set SameSite to Lax to avoid setting invalid cookies. # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#Fixing_common_warnings # noqa: E501 self._cookies[name]['samesite'] = 'Lax' if domain: self._cookies[name]['domain'] = domain if path: self._cookies[name]['path'] = path
def set_cookie(self, name, value, expires=None, max_age=None, domain=None, path=None, secure=None, http_only=True, same_site=None): """Set a response cookie. Note: This method can be called multiple times to add one or more cookies to the response. See Also: To learn more about setting cookies, see :ref:`Setting Cookies <setting-cookies>`. The parameters listed below correspond to those defined in `RFC 6265`_. Args: name (str): Cookie name value (str): Cookie value Keyword Args: expires (datetime): Specifies when the cookie should expire. By default, cookies expire when the user agent exits. (See also: RFC 6265, Section 4.1.2.1) max_age (int): Defines the lifetime of the cookie in seconds. By default, cookies expire when the user agent exits. If both `max_age` and `expires` are set, the latter is ignored by the user agent. Note: Coercion to ``int`` is attempted if provided with ``float`` or ``str``. (See also: RFC 6265, Section 4.1.2.2) domain (str): Restricts the cookie to a specific domain and any subdomains of that domain. By default, the user agent will return the cookie only to the origin server. When overriding this default behavior, the specified domain must include the origin server. Otherwise, the user agent will reject the cookie. Note: Cookies do not provide isolation by port, so the domain should not provide one. (See also: RFC 6265, Section 8.5) (See also: RFC 6265, Section 4.1.2.3) path (str): Scopes the cookie to the given path plus any subdirectories under that path (the "/" character is interpreted as a directory separator). If the cookie does not specify a path, the user agent defaults to the path component of the requested URI. Warning: User agent interfaces do not always isolate cookies by path, and so this should not be considered an effective security measure. (See also: RFC 6265, Section 4.1.2.4) secure (bool): Direct the client to only return the cookie in subsequent requests if they are made over HTTPS (default: ``True``). This prevents attackers from reading sensitive cookie data. Note: The default value for this argument is normally ``True``, but can be modified by setting :py:attr:`~.ResponseOptions.secure_cookies_by_default` via :any:`App.resp_options`. Warning: For the `secure` cookie attribute to be effective, your application will need to enforce HTTPS. (See also: RFC 6265, Section 4.1.2.5) http_only (bool): The HttpOnly attribute limits the scope of the cookie to HTTP requests. In particular, the attribute instructs the user agent to omit the cookie when providing access to cookies via "non-HTTP" APIs. This is intended to mitigate some forms of cross-site scripting. (default: ``True``) Note: HttpOnly cookies are not visible to javascript scripts in the browser. They are automatically sent to the server on javascript ``XMLHttpRequest`` or ``Fetch`` requests. (See also: RFC 6265, Section 4.1.2.6) same_site (str): Helps protect against CSRF attacks by restricting when a cookie will be attached to the request by the user agent. When set to ``'Strict'``, the cookie will only be sent along with "same-site" requests. If the value is ``'Lax'``, the cookie will be sent with same-site requests, and with "cross-site" top-level navigations. If the value is ``'None'``, the cookie will be sent with same-site and cross-site requests. Finally, when this attribute is not set on the cookie, the attribute will be treated as if it had been set to ``'None'``. (See also: `Same-Site RFC Draft`_) Raises: KeyError: `name` is not a valid cookie name. ValueError: `value` is not a valid cookie value. .. _RFC 6265: http://tools.ietf.org/html/rfc6265 .. _Same-Site RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 """ if not is_ascii_encodable(name): raise KeyError('name is not ascii encodable') if not is_ascii_encodable(value): raise ValueError('value is not ascii encodable') value = str(value) if self._cookies is None: self._cookies = http_cookies.SimpleCookie() try: self._cookies[name] = value except http_cookies.CookieError as e: # pragma: no cover # NOTE(tbug): we raise a KeyError here, to avoid leaking # the CookieError to the user. SimpleCookie (well, BaseCookie) # only throws CookieError on issues with the cookie key raise KeyError(str(e)) if expires: # set Expires on cookie. Format is Wdy, DD Mon YYYY HH:MM:SS GMT # NOTE(tbug): we never actually need to # know that GMT is named GMT when formatting cookies. # It is a function call less to just write "GMT" in the fmt string: fmt = '%a, %d %b %Y %H:%M:%S GMT' if expires.tzinfo is None: # naive self._cookies[name]['expires'] = expires.strftime(fmt) else: # aware gmt_expires = expires.astimezone(GMT_TIMEZONE) self._cookies[name]['expires'] = gmt_expires.strftime(fmt) if max_age: # RFC 6265 section 5.2.2 says about the max-age value: # "If the remainder of attribute-value contains a non-DIGIT # character, ignore the cookie-av." # That is, RFC-compliant response parsers will ignore the max-age # attribute if the value contains a dot, as in floating point # numbers. Therefore, attempt to convert the value to an integer. self._cookies[name]['max-age'] = int(max_age) if domain: self._cookies[name]['domain'] = domain if path: self._cookies[name]['path'] = path is_secure = self.options.secure_cookies_by_default if secure is None else secure if is_secure: self._cookies[name]['secure'] = True if http_only: self._cookies[name]['httponly'] = http_only # PERF(kgriffs): Morsel.__setitem__() will lowercase this anyway, # so we can just pass this in and when __setitem__() calls # lower() it will be very slightly faster. if same_site: same_site = same_site.lower() if same_site not in _RESERVED_SAMESITE_VALUES: raise ValueError( "same_site must be set to either 'lax', 'strict', or 'none'" ) self._cookies[name]['samesite'] = same_site.capitalize()