Aug 26 - Enhance security 2
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Regression tests for passkey user verification (review finding #4).
|
||||
|
||||
Both ceremonies used UserVerificationRequirement.PREFERRED with
|
||||
require_user_verification=False, so an authenticator was free to skip the
|
||||
biometric/PIN check. Since a passkey assertion here replaces BOTH the password
|
||||
and the TOTP second factor, that reduced a full login to possession of an
|
||||
unlocked device — enough to enumerate and delete vault items.
|
||||
|
||||
A full ceremony needs a real authenticator, so these tests pin the negotiated
|
||||
options (what the server asks the browser for) and the rejection path. The
|
||||
enforcement half — require_user_verification=True passed to py-webauthn — is
|
||||
asserted directly against the source.
|
||||
"""
|
||||
import inspect
|
||||
import re
|
||||
|
||||
from tests.conftest import auth_headers, make_user
|
||||
|
||||
import app.routes.webauthn as webauthn_routes
|
||||
|
||||
|
||||
def test_registration_options_require_user_verification(client, app):
|
||||
token, _ = make_user(client)
|
||||
res = client.post('/api/webauthn/register/begin',
|
||||
headers=auth_headers(token), json={})
|
||||
assert res.status_code == 200, res.get_json()
|
||||
body = res.get_json()
|
||||
assert body['authenticatorSelection']['userVerification'] == 'required'
|
||||
|
||||
|
||||
def test_authentication_options_require_user_verification(client, app):
|
||||
make_user(client)
|
||||
res = client.post('/api/webauthn/authenticate/begin',
|
||||
json={'email': 'user@example.com'})
|
||||
assert res.status_code == 200, res.get_json()
|
||||
assert res.get_json()['userVerification'] == 'required'
|
||||
|
||||
|
||||
def test_verification_calls_enforce_user_verification():
|
||||
"""
|
||||
Negotiating 'required' is only a request to the browser. The server must
|
||||
also refuse an assertion that comes back without the UV flag set, or the
|
||||
hint is decorative.
|
||||
"""
|
||||
src = inspect.getsource(webauthn_routes)
|
||||
calls = re.findall(r'require_user_verification=(\w+)', src)
|
||||
assert calls, 'no require_user_verification argument found'
|
||||
assert all(v == 'True' for v in calls), (
|
||||
f'require_user_verification must be True everywhere, found: {calls}'
|
||||
)
|
||||
|
||||
|
||||
def test_unknown_credential_is_rejected(client, app):
|
||||
make_user(client)
|
||||
client.post('/api/webauthn/authenticate/begin', json={'email': 'user@example.com'})
|
||||
res = client.post('/api/webauthn/authenticate/complete',
|
||||
json={'id': 'bm9wZQ', 'rawId': 'bm9wZQ'})
|
||||
assert res.status_code == 401
|
||||
assert 'not recognised' in res.get_json()['error']
|
||||
|
||||
|
||||
def test_registration_failure_does_not_leak_exception_text(client, app):
|
||||
"""
|
||||
CLAUDE.md forbids returning str(e) to clients; this handler used to embed
|
||||
the raw py-webauthn message, which quotes attestation internals.
|
||||
"""
|
||||
token, _ = make_user(client)
|
||||
client.post('/api/webauthn/register/begin', headers=auth_headers(token), json={})
|
||||
|
||||
res = client.post('/api/webauthn/register/complete',
|
||||
headers=auth_headers(token), json={'id': 'garbage'})
|
||||
assert res.status_code == 400
|
||||
error = res.get_json()['error']
|
||||
assert error == 'Could not verify this passkey. Please try again.', error
|
||||
|
||||
|
||||
def test_register_complete_requires_a_pending_challenge(client, app):
|
||||
token, _ = make_user(client)
|
||||
res = client.post('/api/webauthn/register/complete',
|
||||
headers=auth_headers(token), json={'id': 'x'})
|
||||
assert res.status_code == 400
|
||||
assert 'No pending registration challenge' in res.get_json()['error']
|
||||
Reference in New Issue
Block a user