Skip to content

Commit 871d67d

Browse files
authored
bug: use better host validation for sub (#92)
While far from perfect, this is a little better about checking the sorts of values passed in the `sub` claim. It checks for ipv6 like as well as localhost in mailto and https forms. (Yeah, you'll still need to supply `https` since that's part of the RFC. Plus, letsEncrypt is a thing, so there's that.) I've also introduced a new option `--no-strict` which turns off `sub` checks. It has to be present, but what the value is can be up to you. I'll note that this kinda ruins what VAPID is supposed to be about. The `sub` is there so that if there's a problem with your subscription, Ops can reach out to you to help fix it rather than just straight up block you. Closes: #90
1 parent f8b063b commit 871d67d

File tree

3 files changed

+51
-7
lines changed

3 files changed

+51
-7
lines changed

python/py_vapid/__init__.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ class Vapid01(object):
3838
_public_key = None
3939
_schema = "WebPush"
4040

41-
def __init__(self, private_key=None):
41+
def __init__(self, private_key=None, conf=None):
4242
"""Initialize VAPID with an optional private key.
4343
4444
:param private_key: A private key object
4545
:type private_key: ec.EllipticCurvePrivateKey
4646
4747
"""
48+
if conf is None:
49+
conf = {}
50+
self.conf = conf
4851
self.private_key = private_key
4952
if private_key:
5053
self._public_key = self.private_key.public_key()
@@ -259,9 +262,11 @@ def _base_sign(self, claims):
259262
cclaims = copy.deepcopy(claims)
260263
if not cclaims.get('exp'):
261264
cclaims['exp'] = str(int(time.time()) + 86400)
262-
if not re.match(r'mailto:.+@.+\..+',
263-
cclaims.get('sub', ''),
264-
re.IGNORECASE):
265+
if not self.conf.get('no-strict', False):
266+
valid = _check_sub(cclaims.get('sub', ''))
267+
else:
268+
valid = cclaims.get('sub') is not None
269+
if not valid:
265270
raise VapidException(
266271
"Missing 'sub' from claims. "
267272
"'sub' is your admin email as a mailto: link.")
@@ -345,5 +350,11 @@ def verify(cls, auth):
345350
verification_token=tokens[1]
346351
)
347352

353+
def _check_sub(sub):
354+
pattern =(
355+
r"^(mailto:.+@((localhost|[%\w]+(\.[%\w]+)+|([0-9a-f]{1,4}):+([0-9a-f]{1,4})?)))|https:\/\/(localhost|\w+\.[\w\.]+|([0-9a-f]{1,4}:+)+([0-9a-f]{1,4})?)$"
356+
)
357+
return re.match(pattern, sub, re.IGNORECASE) is not None
358+
348359

349360
Vapid = Vapid02

python/py_vapid/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def main():
3131
default=False, action="store_true")
3232
parser.add_argument('--json', help="dump as json",
3333
default=False, action="store_true")
34+
parser.add_argumet('--no-strict', help='Do not be strict about "sub"',
35+
default=False, action="store_true")
3436
parser.add_argument('--applicationServerKey',
3537
help="show applicationServerKey value",
3638
default=False, action="store_true")
@@ -52,7 +54,7 @@ def main():
5254
if answer == 'n':
5355
print("Sorry, can't do much for you then.")
5456
exit(1)
55-
vapid = Vapid()
57+
vapid = Vapid(conf=args)
5658
vapid.generate_keys()
5759
print("Generating private_key.pem")
5860
vapid.save_key('private_key.pem')

python/py_vapid/tests/test_vapid.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from cryptography.hazmat.primitives import serialization
88
from mock import patch, Mock
99

10-
from py_vapid import Vapid01, Vapid02, VapidException
10+
from py_vapid import Vapid01, Vapid02, VapidException, _check_sub
1111
from py_vapid.jwt import decode
1212

1313
TEST_KEY_PRIVATE_DER = """
@@ -228,7 +228,12 @@ def test_bad_sign(self):
228228
self.assertRaises(VapidException,
229229
v.sign,
230230
{'sub': 'mailto:[email protected]',
231-
'aud': "https://p.example.com:8080/"})
231+
'aud': "https://p.example.com:8080/"})
232+
233+
def test_ignore_sub(self):
234+
v = Vapid02.from_file("/tmp/private")
235+
v.conf['no-strict'] = True
236+
assert v.sign({"sub": "foo", "aud": "http://localhost:8000"})
232237

233238
@patch('cryptography.hazmat.primitives.asymmetric'
234239
'.ec.EllipticCurvePublicNumbers')
@@ -247,3 +252,29 @@ def test_invalid_sig(self, mm):
247252
decode,
248253
'foo.bar.a',
249254
'aaaa')
255+
256+
def test_sub(self):
257+
valid = [
258+
'mailto:me@localhost',
259+
260+
'mailto:me@1234::',
261+
'mailto:me@1234::5678',
262+
263+
'https://localhost',
264+
'https://8001::',
265+
'https://8001:1000:0001',
266+
'https://1.2.3.4'
267+
]
268+
invalid = [
269+
'mailto:@foobar.com',
270+
'mailto:example.org',
271+
'mailto:0123:',
272+
'mailto:::1234',
273+
'https://somehost',
274+
'https://xyz:123',
275+
]
276+
277+
for val in valid:
278+
assert _check_sub(val) is True
279+
for val in invalid:
280+
assert _check_sub(val) is False

0 commit comments

Comments
 (0)