cookies: Don't encrypt the unlogged-user _options cookie.

This cookie is part of the CDN cache key. Since the encrypted blob uses
a random salt, this essentially makes requests with the cookie
unchacheable. The cookie does not contain any sensitive data (just a
list of languages and a boolean True/False). Decrypting and normalizing
the order of params means that the cache key will be more useful. We
also get a fair reduction in size of the cookie.
This commit is contained in:
Neil Williams
2013-01-18 16:04:54 -08:00
parent 77ee82cf9c
commit 7e715b1197
2 changed files with 68 additions and 15 deletions

View File

@@ -21,6 +21,7 @@
###############################################################################
import collections
import json
import locale
import re
import simplejson
@@ -75,6 +76,7 @@ from r2.lib.validator import (
validate,
VByName,
VCount,
VLang,
VLength,
VLimit,
VTarget,
@@ -153,8 +155,12 @@ class Cookie(object):
% (self.value, self.expires, self.domain, self.dirty))
class UnloggedUser(FakeAccount):
_cookie = 'options'
allowed_prefs = ('pref_content_langs', 'pref_lang', 'pref_frame_commentspanel')
COOKIE_NAME = "_options"
allowed_prefs = {
"pref_lang": VLang.validate_lang,
"pref_content_langs": VLang.validate_content_langs,
"pref_frame_commentspanel": bool,
}
def __init__(self, browser_langs, *a, **kw):
FakeAccount.__init__(self, *a, **kw)
@@ -178,21 +184,43 @@ class UnloggedUser(FakeAccount):
def name(self):
raise NotImplementedError
def _decode_json(self, json_blob):
data = json.loads(json_blob)
validated = {}
for k, v in data.iteritems():
validator = self.allowed_prefs.get(k)
if validator:
try:
validated[k] = validator(v)
except ValueError:
pass # don't override defaults for bad data
return validated
def _from_cookie(self):
z = read_user_cookie(self._cookie)
try:
d = simplejson.loads(decrypt(z))
return dict((k, v) for k, v in d.iteritems()
if k in self.allowed_prefs)
except (ValueError, TypeError):
cookie = c.cookies.get(self.COOKIE_NAME)
if not cookie:
return {}
try:
return self._decode_json(cookie.value)
except ValueError:
# old-style _options cookies are encrypted
try:
plaintext = decrypt(cookie.value)
values = self._decode_json(plaintext)
except (TypeError, ValueError):
# this cookie is totally invalid, delete it
c.cookies[self.COOKIE_NAME] = Cookie(value="", expires=DELETE)
return {}
else:
self._to_cookie(values) # upgrade the cookie
return values
def _to_cookie(self, data):
data = data.copy()
for k in data.keys():
if k not in self.allowed_prefs:
del k
set_user_cookie(self._cookie, encrypt(simplejson.dumps(data)))
allowed_data = {k: v for k, v in data.iteritems()
if k in self.allowed_prefs}
jsonified = json.dumps(allowed_data, sort_keys=True)
c.cookies[self.COOKIE_NAME] = Cookie(value=jsonified)
def _subscribe(self, sr):
pass

View File

@@ -314,10 +314,35 @@ class nop(Validator):
return x
class VLang(Validator):
def run(self, lang):
@staticmethod
def validate_lang(lang, strict=False):
if lang in g.all_languages:
return lang
return g.lang
else:
if not strict:
return g.lang
else:
raise ValueError("invalid language %r" % lang)
@staticmethod
def validate_content_langs(langs):
if langs == "all":
return langs
validated = []
for lang in langs:
try:
validated.append(VLang.validate_lang(lang, strict=True))
except ValueError:
pass
if not validated:
raise ValueError("no valid languages")
return validated
def run(self, lang):
return VLang.validate_lang(lang)
class VRequired(Validator):
def __init__(self, param, error, *a, **kw):