Skip to content

Commit f31a80f

Browse files
authored
Merge pull request #2348 from aboutcode-org/altcha-timeout
Improve altcha challenge flow and reduce session timeout
2 parents d802717 + 7d4339a commit f31a80f

8 files changed

Lines changed: 91 additions & 28 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Version v39.0.0
2828
- Add ALTCHA verification in UI
2929
- Add API/ UI support for Patch/PackageCommitPatch
3030
- Fix failing V2 pipelinea
31+
- Improve altcha challenge flow and reduce session timeout (https://github.com/aboutcode-org/vulnerablecode/pull/2348)
3132

3233

3334
Version v38.6.0

vulnerabilities/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,4 @@ class AdvisoryToDoForm(forms.Form):
136136

137137

138138
class AltchaForm(forms.Form):
139-
altcha = AltchaField()
139+
altcha = AltchaField(auto="onload")

vulnerabilities/middleware/altcha_protection.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,39 @@
88
#
99

1010
import time
11+
from urllib.parse import urlencode
1112

1213
from django.shortcuts import redirect
1314
from django.utils.deprecation import MiddlewareMixin
1415

16+
SESSION_TIMEOUT = 900 # 15 minutes
17+
18+
ALTCHA_PROTECTED_PREFIXES = (
19+
"/packages/",
20+
"/vulnerabilities/",
21+
"/advisories/",
22+
"/affected-by-advisories/v2/",
23+
"/fixing-advisories/v2/",
24+
"/pipelines/",
25+
)
1526

16-
class AltchaProtectionMiddleware(MiddlewareMixin):
17-
PROTECTED_PREFIXES = (
18-
"/packages/",
19-
"/vulnerabilities/",
20-
"/advisories/",
21-
"/affected-by-advisories/v2/",
22-
"/fixing-advisories/v2/",
23-
)
2427

25-
SESSION_TIMEOUT = 3600 # 1 hour
28+
class AltchaProtectionMiddleware(MiddlewareMixin):
2629

2730
def __call__(self, request):
28-
protected = any(request.path.startswith(prefix) for prefix in self.PROTECTED_PREFIXES)
31+
protected = any(request.path.startswith(prefix) for prefix in ALTCHA_PROTECTED_PREFIXES)
2932

3033
if not protected:
3134
return self.get_response(request)
3235

3336
verified_at = request.session.get("altcha_verified_at")
37+
next_url = request.get_full_path()
3438

3539
if not verified_at:
36-
return redirect(f"/altcha/")
40+
return redirect(f"/altcha/?{urlencode({'next': next_url})}")
3741

38-
if time.time() - verified_at > self.SESSION_TIMEOUT:
42+
if time.time() - verified_at > SESSION_TIMEOUT:
3943
request.session.pop("altcha_verified_at", None)
40-
return redirect(f"/altcha/")
44+
return redirect(f"/altcha/?{urlencode({'next': next_url})}")
4145

4246
return self.get_response(request)

vulnerabilities/migrations/0137_alter_pipelineschedule_run_interval.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def revert_convert_hours_to_minutes(apps, schema_editor):
2626
migrations.AlterField(
2727
model_name="pipelineschedule",
2828
name="run_interval",
29-
field=models.PositiveSmallIntegerField(
29+
field=models.IntegerField(
3030
default=1440,
3131
help_text="Number of minutes to wait between run of this pipeline.",
3232
validators=[
Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
1-
<form method="post">
2-
{% csrf_token %}
3-
{{ form }}
4-
<button type="submit">Continue</button>
5-
</form>
1+
{% extends "base.html" %}
2+
{% load static %}
3+
4+
{% block content %}
5+
<section class="section">
6+
<div class="columns is-centered mt-4 pt-4">
7+
<div class="column is-4">
8+
<div class="card">
9+
<div class="card-content">
10+
<h1 class="title is-5 has-text-centered mt-4">
11+
Please verify that you are human to continue
12+
</h1>
13+
14+
<form method="post">
15+
{% csrf_token %}
16+
17+
<div class="has-text-centered ml-5 pt-4">
18+
<div style="display:inline-block; min-width: 304px;">
19+
{{ form }}
20+
</div>
21+
</div>
22+
23+
<div class="field mt-4 pt-2">
24+
<div class="control">
25+
<button type="submit" class="button is-info is-fullwidth">
26+
Continue
27+
</button>
28+
</div>
29+
</div>
30+
</form>
31+
</div>
32+
</div>
33+
</div>
34+
</div>
35+
</section>
36+
{% endblock %}

vulnerabilities/tests/test_altcha_protection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_protected_url_redirects_without_session(self, client):
2222
response = client.get("/packages/search/")
2323

2424
assert response.status_code == 302
25-
assert response.url == "/altcha/"
25+
assert response.url == "/altcha/?next=%2Fpackages%2Fsearch%2F"
2626

2727
def test_unprotected_url_is_accessible(self, client):
2828
response = client.get("/")
@@ -46,7 +46,7 @@ def test_expired_session_redirects(self, client):
4646
response = client.get("/packages/search/")
4747

4848
assert response.status_code == 302
49-
assert response.url == "/altcha/"
49+
assert response.url == "/altcha/?next=%2Fpackages%2Fsearch%2F"
5050

5151
def test_expired_session_is_removed(self, client):
5252
session = client.session

vulnerabilities/utils.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
from functools import total_ordering
2121
from http import HTTPStatus
2222
from typing import List
23-
from typing import NamedTuple
2423
from typing import Optional
25-
from typing import Set
2624
from typing import Tuple
2725
from typing import Union
2826
from unittest.mock import MagicMock
27+
from urllib.parse import unquote
2928
from urllib.parse import urljoin
3029

3130
import dateparser
@@ -36,6 +35,8 @@
3635
from cwe2.database import Database
3736
from cwe2.database import InvalidCWEError
3837
from django.db.models import Prefetch
38+
from django.shortcuts import redirect
39+
from django.utils.http import url_has_allowed_host_and_scheme
3940
from packageurl import PackageURL
4041
from packageurl.contrib.django.utils import without_empty_values
4142
from univers.version_range import RANGE_CLASS_BY_SCHEMES
@@ -44,6 +45,7 @@
4445
from univers.version_range import VersionRange
4546

4647
from aboutcode.hashid import build_vcid
48+
from vulnerabilities.middleware.altcha_protection import ALTCHA_PROTECTED_PREFIXES
4749

4850
logger = logging.getLogger(__name__)
4951

@@ -1114,3 +1116,16 @@ def build_alias_to_advisory_map(aliases_strs):
11141116
):
11151117
alias_to_advisories[advisory.advisory_id].add(advisory)
11161118
return alias_to_advisories
1119+
1120+
1121+
def safe_altcha_redirect(next_url: str) -> redirect:
1122+
"""Safely redirect to Altcha protected URL, block external URLs."""
1123+
is_safe = url_has_allowed_host_and_scheme(url=next_url, allowed_hosts=None, require_https=False)
1124+
1125+
decoded_url = unquote(next_url) if next_url else ""
1126+
is_protected = any(decoded_url.startswith(prefix) for prefix in ALTCHA_PROTECTED_PREFIXES)
1127+
1128+
if is_safe and is_protected:
1129+
return redirect(next_url)
1130+
1131+
return redirect("/")

vulnerabilities/views.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from cvss.exceptions import CVSS2MalformedError
1717
from cvss.exceptions import CVSS3MalformedError
1818
from cvss.exceptions import CVSS4MalformedError
19-
from django import forms
2019
from django.contrib import messages
2120
from django.contrib.auth.views import LoginView
2221
from django.core.cache import cache
@@ -29,7 +28,6 @@
2928
from django.http import HttpResponse
3029
from django.http.response import Http404
3130
from django.shortcuts import get_object_or_404
32-
from django.shortcuts import redirect
3331
from django.shortcuts import render
3432
from django.urls import reverse_lazy
3533
from django.views import View
@@ -38,7 +36,6 @@
3836
from django.views.generic.edit import FormMixin
3937
from django.views.generic.edit import FormView
4038
from django.views.generic.list import ListView
41-
from django_altcha import AltchaField
4239

4340
from vulnerabilities import models
4441
from vulnerabilities.forms import AdminLoginForm
@@ -49,6 +46,7 @@
4946
from vulnerabilities.forms import PackageSearchForm
5047
from vulnerabilities.forms import PipelineSchedulePackageForm
5148
from vulnerabilities.forms import VulnerabilitySearchForm
49+
from vulnerabilities.middleware.altcha_protection import SESSION_TIMEOUT as ALTCHA_SESSION_TIMEOUT
5250
from vulnerabilities.models import ISSUE_TYPE_CHOICES
5351
from vulnerabilities.models import AdvisorySetMember
5452
from vulnerabilities.models import AdvisoryToDoV2
@@ -66,6 +64,7 @@
6664
from vulnerabilities.throttling import AnonUserUIThrottle
6765
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
6866
from vulnerabilities.utils import get_advisories_from_groups
67+
from vulnerabilities.utils import safe_altcha_redirect
6968
from vulnerablecode import __version__ as VULNERABLECODE_VERSION
7069
from vulnerablecode.settings import env
7170

@@ -1165,6 +1164,19 @@ class AltchaView(FormView):
11651164
template_name = "altcha.html"
11661165
form_class = AltchaForm
11671166

1167+
def dispatch(self, request, *args, **kwargs):
1168+
"""Do not show Altcha challenge to already validated user."""
1169+
verified_at = request.session.get("altcha_verified_at")
1170+
1171+
if verified_at:
1172+
if time.time() - verified_at < ALTCHA_SESSION_TIMEOUT:
1173+
next_url = request.GET.get("next", "/")
1174+
return safe_altcha_redirect(next_url)
1175+
1176+
return super().dispatch(request, *args, **kwargs)
1177+
11681178
def form_valid(self, form):
11691179
self.request.session["altcha_verified_at"] = time.time()
1170-
return redirect("/")
1180+
1181+
next_url = self.request.GET.get("next", "/")
1182+
return safe_altcha_redirect(next_url)

0 commit comments

Comments
 (0)