From 112b3518196a4581c461ee4917da1251b30b3fa1 Mon Sep 17 00:00:00 2001 From: Benno Date: Wed, 30 Oct 2024 22:22:22 +0100 Subject: [PATCH 1/4] Implement "next" redirect logic after successful passkey login --- frontend/passkeys.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/passkeys.js b/frontend/passkeys.js index 18bb7b3..3be9999 100644 --- a/frontend/passkeys.js +++ b/frontend/passkeys.js @@ -15,7 +15,12 @@ const sendToServerForVerificationAndLogin = async (response) => { // Show UI appropriate for the `verified` status if (verificationJSON && verificationJSON.verified) { console.log("success") - window.location.href = '/profile'; + const urlParams = new URLSearchParams(window.location.search); + let nextUrl = urlParams.get('next'); + if (nextUrl === null) { + nextUrl = '/profile'; + } + window.location.href = nextUrl; } else { console.log("error", verificationJSON); } From cf8f1b34311a51c5772f9acf16a25c9505b7ecae Mon Sep 17 00:00:00 2001 From: Benno Date: Wed, 30 Oct 2024 22:33:46 +0100 Subject: [PATCH 2/4] Update lockfile with uv to match Dependabot Django update --- uv.lock | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 2eee943..30033c8 100644 --- a/uv.lock +++ b/uv.lock @@ -164,16 +164,16 @@ wheels = [ [[package]] name = "django" -version = "4.2.15" +version = "5.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/1e/d6306fb0a4f44ed9d12430953786747d609c85258c74c545fdc2b37f5a83/Django-4.2.15.tar.gz", hash = "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a", size = 10418066 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/e5/a06e20c963b280af4aa9432bc694fbdeb1c8df9e28c2ffd5fbb71c4b1bec/Django-5.1.2.tar.gz", hash = "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", size = 10711674 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/77/86af525feb6a9951d2bc96371ae3652f11bb35933b21966abc594f777956/Django-4.2.15-py3-none-any.whl", hash = "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", size = 7992797 }, + { url = "https://files.pythonhosted.org/packages/a3/b8/f205f2b8c44c6cdc555c4f56bbe85ceef7f67c0cf1caa8abe078bb7e32bd/Django-5.1.2-py3-none-any.whl", hash = "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed", size = 8276058 }, ] [[package]] @@ -472,13 +472,14 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "testcontainers" }, { name = "testcontainers", extra = ["mysql"] }, { name = "unittest-xml-reporting" }, ] [package.metadata] requires-dist = [ - { name = "django", specifier = "~=4.2" }, + { name = "django", specifier = "~=5.1" }, { name = "django-bootstrap5", specifier = "~=24.2" }, { name = "django-crontab", specifier = "~=0.7.1" }, { name = "django-environ", specifier = "~=0.11.2" }, @@ -552,6 +553,7 @@ wheels = [ [package.optional-dependencies] mysql = [ + { name = "pymysql" }, { name = "pymysql", extra = ["rsa"] }, { name = "sqlalchemy" }, ] From 0e3f4836271aaff01cad4cc3989c32734caa64d5 Mon Sep 17 00:00:00 2001 From: Benno Date: Wed, 30 Oct 2024 22:36:27 +0100 Subject: [PATCH 3/4] Fix login link logic after Django 5 migration: Use custom database column type as instruced by Django Docs --- .../migrations/0002_alter_loginlink_code.py | 20 +++++++++++++++++++ spybot/models.py | 15 +++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 spybot/migrations/0002_alter_loginlink_code.py diff --git a/spybot/migrations/0002_alter_loginlink_code.py b/spybot/migrations/0002_alter_loginlink_code.py new file mode 100644 index 0000000..ac62524 --- /dev/null +++ b/spybot/migrations/0002_alter_loginlink_code.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.15 on 2024-10-30 21:30 + +from django.db import migrations +import spybot.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('spybot', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='loginlink', + name='code', + field=spybot.models.Char32UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/spybot/models.py b/spybot/models.py index 73e7e94..bc74f25 100644 --- a/spybot/models.py +++ b/spybot/models.py @@ -195,9 +195,22 @@ class NewsEvent(DebuggableModel): date = models.DateTimeField(auto_now_add=True) +# fix for Django 5 UUID column behavior change: +# https://docs.djangoproject.com/en/5.0/releases/5.0/#migrating-existing-uuidfield-on-mariadb-10-7 +class Char32UUIDField(models.UUIDField): + def db_type(self, connection): + return "char(32)" + + def get_db_prep_value(self, value, connection, prepared=False): + value = super().get_db_prep_value(value, connection, prepared) + if value is not None: + value = value.hex + return value + + class LoginLink(DebuggableModel): user = models.ForeignKey(MergedUser, models.CASCADE, blank=False, null=False, related_name="loginlinks") - code = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + code = Char32UUIDField(default=uuid.uuid4, editable=False, unique=True) class UserPasskey(models.Model): From 424f41efc8193866a7f67ff3b0fb3cc644769de4 Mon Sep 17 00:00:00 2001 From: Benno Date: Wed, 30 Oct 2024 22:50:00 +0100 Subject: [PATCH 4/4] Fix potential XSS vulnerability in post-login redirect logic --- frontend/passkeys.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/passkeys.js b/frontend/passkeys.js index 3be9999..54de2e9 100644 --- a/frontend/passkeys.js +++ b/frontend/passkeys.js @@ -1,5 +1,10 @@ import {startAuthentication, startRegistration} from '@simplewebauthn/browser' +const isAllowedRedirectUrl = url => { + const regex = /^[A-Za-z0-9/]+$/; + return regex.test(str); +} + const sendToServerForVerificationAndLogin = async (response) => { try { console.log("sendToServerForVerificationAndLogin:", response); @@ -17,7 +22,7 @@ const sendToServerForVerificationAndLogin = async (response) => { console.log("success") const urlParams = new URLSearchParams(window.location.search); let nextUrl = urlParams.get('next'); - if (nextUrl === null) { + if (nextUrl === null || !isAllowedRedirectUrl(nextUrl)) { nextUrl = '/profile'; } window.location.href = nextUrl;