From 274540e030728fc32cf55c6618361c33b0763d85 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 13:36:29 +0530 Subject: [PATCH 1/8] feat: reduce altcha session timeout to 15 minutes Signed-off-by: Keshav Priyadarshi --- vulnerabilities/middleware/altcha_protection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/middleware/altcha_protection.py b/vulnerabilities/middleware/altcha_protection.py index 2bfcf6503..556525302 100644 --- a/vulnerabilities/middleware/altcha_protection.py +++ b/vulnerabilities/middleware/altcha_protection.py @@ -22,7 +22,7 @@ class AltchaProtectionMiddleware(MiddlewareMixin): "/fixing-advisories/v2/", ) - SESSION_TIMEOUT = 3600 # 1 hour + SESSION_TIMEOUT = 900 # 15 minutes def __call__(self, request): protected = any(request.path.startswith(prefix) for prefix in self.PROTECTED_PREFIXES) From b8f7772b852e4b049bc97c8cc19b9f91cf04b691 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 13:52:16 +0530 Subject: [PATCH 2/8] feat(ui): add proper template to Altcha challenge page Signed-off-by: Keshav Priyadarshi --- vulnerabilities/forms.py | 2 +- vulnerabilities/templates/altcha.html | 41 +++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/vulnerabilities/forms.py b/vulnerabilities/forms.py index 0f46cd871..122f8125a 100644 --- a/vulnerabilities/forms.py +++ b/vulnerabilities/forms.py @@ -136,4 +136,4 @@ class AdvisoryToDoForm(forms.Form): class AltchaForm(forms.Form): - altcha = AltchaField() + altcha = AltchaField(auto="onload") diff --git a/vulnerabilities/templates/altcha.html b/vulnerabilities/templates/altcha.html index 071f625e0..907926b47 100644 --- a/vulnerabilities/templates/altcha.html +++ b/vulnerabilities/templates/altcha.html @@ -1,5 +1,36 @@ -
- {% csrf_token %} - {{ form }} - -
\ No newline at end of file +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+
+
+
+

+ Please verify that you are human to continue +

+ +
+ {% csrf_token %} + +
+
+ {{ form }} +
+
+ +
+
+ +
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file From 9d0ca0fda6c284a93403e144de83e28aa3405ed7 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 14:02:16 +0530 Subject: [PATCH 3/8] feat: redirect user to requested URL after successful altcha verification Signed-off-by: Keshav Priyadarshi --- vulnerabilities/middleware/altcha_protection.py | 13 ++++++++----- vulnerabilities/views.py | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/vulnerabilities/middleware/altcha_protection.py b/vulnerabilities/middleware/altcha_protection.py index 556525302..e05eec262 100644 --- a/vulnerabilities/middleware/altcha_protection.py +++ b/vulnerabilities/middleware/altcha_protection.py @@ -8,10 +8,13 @@ # import time +from urllib.parse import urlencode from django.shortcuts import redirect from django.utils.deprecation import MiddlewareMixin +SESSION_TIMEOUT = 900 # 15 minutes + class AltchaProtectionMiddleware(MiddlewareMixin): PROTECTED_PREFIXES = ( @@ -20,10 +23,9 @@ class AltchaProtectionMiddleware(MiddlewareMixin): "/advisories/", "/affected-by-advisories/v2/", "/fixing-advisories/v2/", + "/pipelines/", ) - SESSION_TIMEOUT = 900 # 15 minutes - def __call__(self, request): protected = any(request.path.startswith(prefix) for prefix in self.PROTECTED_PREFIXES) @@ -31,12 +33,13 @@ def __call__(self, request): return self.get_response(request) verified_at = request.session.get("altcha_verified_at") + next_url = request.get_full_path() if not verified_at: - return redirect(f"/altcha/") + return redirect(f"/altcha/?{urlencode({'next': next_url})}") - if time.time() - verified_at > self.SESSION_TIMEOUT: + if time.time() - verified_at > SESSION_TIMEOUT: request.session.pop("altcha_verified_at", None) - return redirect(f"/altcha/") + return redirect(f"/altcha/?{urlencode({'next': next_url})}") return self.get_response(request) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 3c11ea556..011d0b99b 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -1167,4 +1167,6 @@ class AltchaView(FormView): def form_valid(self, form): self.request.session["altcha_verified_at"] = time.time() - return redirect("/") + + next_url = self.request.GET.get("next", "/") + return redirect(next_url) From 7f9bbd7ad9b16b1a79e9fcee8e2ec00495e0f96e Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 14:09:33 +0530 Subject: [PATCH 4/8] feat: skip altcha challenge for already validated user Signed-off-by: Keshav Priyadarshi --- vulnerabilities/views.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 011d0b99b..62231366c 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -49,6 +49,7 @@ from vulnerabilities.forms import PackageSearchForm from vulnerabilities.forms import PipelineSchedulePackageForm from vulnerabilities.forms import VulnerabilitySearchForm +from vulnerabilities.middleware.altcha_protection import SESSION_TIMEOUT as ALTCHA_SESSION_TIMEOUT from vulnerabilities.models import ISSUE_TYPE_CHOICES from vulnerabilities.models import AdvisorySetMember from vulnerabilities.models import AdvisoryToDoV2 @@ -1165,6 +1166,17 @@ class AltchaView(FormView): template_name = "altcha.html" form_class = AltchaForm + def dispatch(self, request, *args, **kwargs): + """Do not show Altcha challenge to already validated user.""" + verified_at = request.session.get("altcha_verified_at") + + if verified_at: + if time.time() - verified_at < ALTCHA_SESSION_TIMEOUT: + next_url = request.GET.get("next", "/") + return redirect(next_url) + + return super().dispatch(request, *args, **kwargs) + def form_valid(self, form): self.request.session["altcha_verified_at"] = time.time() From ededf43669f1ae056bba3c51424fb262ab0c17fd Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 14:15:28 +0530 Subject: [PATCH 5/8] test: update Altcha protection test fixtures Signed-off-by: Keshav Priyadarshi --- vulnerabilities/tests/test_altcha_protection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/tests/test_altcha_protection.py b/vulnerabilities/tests/test_altcha_protection.py index f7f209c95..0232c3511 100644 --- a/vulnerabilities/tests/test_altcha_protection.py +++ b/vulnerabilities/tests/test_altcha_protection.py @@ -22,7 +22,7 @@ def test_protected_url_redirects_without_session(self, client): response = client.get("/packages/search/") assert response.status_code == 302 - assert response.url == "/altcha/" + assert response.url == "/altcha/?next=%2Fpackages%2Fsearch%2F" def test_unprotected_url_is_accessible(self, client): response = client.get("/") @@ -46,7 +46,7 @@ def test_expired_session_redirects(self, client): response = client.get("/packages/search/") assert response.status_code == 302 - assert response.url == "/altcha/" + assert response.url == "/altcha/?next=%2Fpackages%2Fsearch%2F" def test_expired_session_is_removed(self, client): session = client.session From f9e5df1dc1ed5254dc314a3bd62c93df2d3fec16 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 17:39:19 +0530 Subject: [PATCH 6/8] fix(security): sanitize redirect URLs after altcha validation Signed-off-by: Keshav Priyadarshi --- .../middleware/altcha_protection.py | 19 ++++++++++--------- vulnerabilities/utils.py | 19 +++++++++++++++++-- vulnerabilities/views.py | 8 +++----- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/vulnerabilities/middleware/altcha_protection.py b/vulnerabilities/middleware/altcha_protection.py index e05eec262..ed1db446e 100644 --- a/vulnerabilities/middleware/altcha_protection.py +++ b/vulnerabilities/middleware/altcha_protection.py @@ -15,19 +15,20 @@ SESSION_TIMEOUT = 900 # 15 minutes +ALTCHA_PROTECTED_PREFIXES = ( + "/packages/", + "/vulnerabilities/", + "/advisories/", + "/affected-by-advisories/v2/", + "/fixing-advisories/v2/", + "/pipelines/", +) + class AltchaProtectionMiddleware(MiddlewareMixin): - PROTECTED_PREFIXES = ( - "/packages/", - "/vulnerabilities/", - "/advisories/", - "/affected-by-advisories/v2/", - "/fixing-advisories/v2/", - "/pipelines/", - ) def __call__(self, request): - protected = any(request.path.startswith(prefix) for prefix in self.PROTECTED_PREFIXES) + protected = any(request.path.startswith(prefix) for prefix in ALTCHA_PROTECTED_PREFIXES) if not protected: return self.get_response(request) diff --git a/vulnerabilities/utils.py b/vulnerabilities/utils.py index ff229a5f1..a4fe0274f 100644 --- a/vulnerabilities/utils.py +++ b/vulnerabilities/utils.py @@ -20,12 +20,11 @@ from functools import total_ordering from http import HTTPStatus from typing import List -from typing import NamedTuple from typing import Optional -from typing import Set from typing import Tuple from typing import Union from unittest.mock import MagicMock +from urllib.parse import unquote from urllib.parse import urljoin import dateparser @@ -36,6 +35,8 @@ from cwe2.database import Database from cwe2.database import InvalidCWEError from django.db.models import Prefetch +from django.shortcuts import redirect +from django.utils.http import url_has_allowed_host_and_scheme from packageurl import PackageURL from packageurl.contrib.django.utils import without_empty_values from univers.version_range import RANGE_CLASS_BY_SCHEMES @@ -44,6 +45,7 @@ from univers.version_range import VersionRange from aboutcode.hashid import build_vcid +from vulnerabilities.middleware.altcha_protection import ALTCHA_PROTECTED_PREFIXES logger = logging.getLogger(__name__) @@ -1114,3 +1116,16 @@ def build_alias_to_advisory_map(aliases_strs): ): alias_to_advisories[advisory.advisory_id].add(advisory) return alias_to_advisories + + +def safe_altcha_redirect(next_url: str) -> redirect: + """Safely redirect to Altcha protected URL, block external URLs.""" + is_safe = url_has_allowed_host_and_scheme(url=next_url, allowed_hosts=None, require_https=False) + + decoded_url = unquote(next_url) if next_url else "" + is_protected = any(decoded_url.startswith(prefix) for prefix in ALTCHA_PROTECTED_PREFIXES) + + if is_safe and is_protected: + return redirect(next_url) + + return redirect("/") diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 62231366c..5d69b4a57 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -16,7 +16,6 @@ from cvss.exceptions import CVSS2MalformedError from cvss.exceptions import CVSS3MalformedError from cvss.exceptions import CVSS4MalformedError -from django import forms from django.contrib import messages from django.contrib.auth.views import LoginView from django.core.cache import cache @@ -29,7 +28,6 @@ from django.http import HttpResponse from django.http.response import Http404 from django.shortcuts import get_object_or_404 -from django.shortcuts import redirect from django.shortcuts import render from django.urls import reverse_lazy from django.views import View @@ -38,7 +36,6 @@ from django.views.generic.edit import FormMixin from django.views.generic.edit import FormView from django.views.generic.list import ListView -from django_altcha import AltchaField from vulnerabilities import models from vulnerabilities.forms import AdminLoginForm @@ -67,6 +64,7 @@ from vulnerabilities.throttling import AnonUserUIThrottle from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS from vulnerabilities.utils import get_advisories_from_groups +from vulnerabilities.utils import safe_altcha_redirect from vulnerablecode import __version__ as VULNERABLECODE_VERSION from vulnerablecode.settings import env @@ -1173,7 +1171,7 @@ def dispatch(self, request, *args, **kwargs): if verified_at: if time.time() - verified_at < ALTCHA_SESSION_TIMEOUT: next_url = request.GET.get("next", "/") - return redirect(next_url) + return safe_altcha_redirect(next_url) return super().dispatch(request, *args, **kwargs) @@ -1181,4 +1179,4 @@ def form_valid(self, form): self.request.session["altcha_verified_at"] = time.time() next_url = self.request.GET.get("next", "/") - return redirect(next_url) + return safe_altcha_redirect(next_url) From 88b9497c9b5f418173c766ad6a8eb3428a230a47 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 17:45:00 +0530 Subject: [PATCH 7/8] docs: update CHANGELOG Signed-off-by: Keshav Priyadarshi --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 46b9573e6..48be79c22 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,6 +28,7 @@ Version v39.0.0 - Add ALTCHA verification in UI - Add API/ UI support for Patch/PackageCommitPatch - Fix failing V2 pipelinea +- Improve altcha challenge flow and reduce session timeout (https://github.com/aboutcode-org/vulnerablecode/pull/2348) Version v38.6.0 From 7d4339a5b1b0b7e6547b463293366a3312417fe6 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 17 Jun 2026 17:56:57 +0530 Subject: [PATCH 8/8] fix: use IntegerField for run_interval migration Signed-off-by: Keshav Priyadarshi --- .../migrations/0137_alter_pipelineschedule_run_interval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/migrations/0137_alter_pipelineschedule_run_interval.py b/vulnerabilities/migrations/0137_alter_pipelineschedule_run_interval.py index 0f440ff8d..1b31e008a 100644 --- a/vulnerabilities/migrations/0137_alter_pipelineschedule_run_interval.py +++ b/vulnerabilities/migrations/0137_alter_pipelineschedule_run_interval.py @@ -26,7 +26,7 @@ def revert_convert_hours_to_minutes(apps, schema_editor): migrations.AlterField( model_name="pipelineschedule", name="run_interval", - field=models.PositiveSmallIntegerField( + field=models.IntegerField( default=1440, help_text="Number of minutes to wait between run of this pipeline.", validators=[