Skip to content

Commit 41acdf7

Browse files
committed
Add ALTCHA verification in UI
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 9874639 commit 41acdf7

9 files changed

Lines changed: 173 additions & 2 deletions

File tree

vulnerabilities/forms.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,7 @@ class AdvisoryToDoForm(forms.Form):
133133
choices=[("", "All")] + ISSUE_TYPE_CHOICES,
134134
widget=forms.Select(attrs={"class": "select"}),
135135
)
136+
137+
138+
class AltchaForm(forms.Form):
139+
altcha = AltchaField()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import time
11+
12+
from django.shortcuts import redirect
13+
from django.utils.deprecation import MiddlewareMixin
14+
15+
16+
class AltchaProtectionMiddleware(MiddlewareMixin):
17+
PROTECTED_PREFIXES = (
18+
"/packages/",
19+
"/vulnerabilities/",
20+
"/advisories/",
21+
"/affected-by-advisories/v2/",
22+
"/fixing-advisories/v2/",
23+
)
24+
25+
SESSION_TIMEOUT = 3600 # 1 hour
26+
27+
def __call__(self, request):
28+
protected = any(request.path.startswith(prefix) for prefix in self.PROTECTED_PREFIXES)
29+
30+
if not protected:
31+
return self.get_response(request)
32+
33+
verified_at = request.session.get("altcha_verified_at")
34+
35+
if not verified_at:
36+
return redirect(f"/altcha/")
37+
38+
if time.time() - verified_at > self.SESSION_TIMEOUT:
39+
request.session.pop("altcha_verified_at", None)
40+
return redirect(f"/altcha/")
41+
42+
return self.get_response(request)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<form method="post">
2+
{% csrf_token %}
3+
{{ form }}
4+
<button type="submit">Continue</button>
5+
</form>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import time
11+
12+
import pytest
13+
from django.test import RequestFactory
14+
15+
from vulnerabilities.forms import AltchaForm
16+
from vulnerabilities.views import AltchaView
17+
18+
19+
@pytest.mark.django_db
20+
class TestAltchaProtectionMiddleware:
21+
def test_protected_url_redirects_without_session(self, client):
22+
response = client.get("/packages/search/")
23+
24+
assert response.status_code == 302
25+
assert response.url == "/altcha/"
26+
27+
def test_unprotected_url_is_accessible(self, client):
28+
response = client.get("/")
29+
30+
assert response.status_code != 302
31+
32+
def test_protected_url_allowed_with_valid_session(self, client):
33+
session = client.session
34+
session["altcha_verified_at"] = time.time()
35+
session.save()
36+
37+
response = client.get("/packages/search/")
38+
39+
assert response.status_code != 302
40+
41+
def test_expired_session_redirects(self, client):
42+
session = client.session
43+
session["altcha_verified_at"] = time.time() - 3601
44+
session.save()
45+
46+
response = client.get("/packages/search/")
47+
48+
assert response.status_code == 302
49+
assert response.url == "/altcha/"
50+
51+
def test_expired_session_is_removed(self, client):
52+
session = client.session
53+
session["altcha_verified_at"] = time.time() - 3601
54+
session.save()
55+
56+
client.get("/packages/search/")
57+
58+
session = client.session
59+
assert "altcha_verified_at" not in session
60+
61+
62+
@pytest.mark.django_db
63+
class TestAltchaView:
64+
def test_form_valid_sets_session(self, monkeypatch):
65+
now = 1234567890
66+
67+
monkeypatch.setattr(time, "time", lambda: now)
68+
69+
request = RequestFactory().get("/altcha/")
70+
request.session = {}
71+
72+
view = AltchaView()
73+
view.request = request
74+
75+
response = view.form_valid(AltchaForm())
76+
77+
assert response.status_code == 302
78+
assert response.url == "/"
79+
assert request.session["altcha_verified_at"] == now

vulnerabilities/tests/test_forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import time
11+
12+
from django.test import Client
1013
from django.test import TestCase
1114

1215
from vulnerabilities.forms import VulnerabilitySearchForm
@@ -15,6 +18,10 @@
1518

1619
class TestVulnerabilitySearchForm(TestCase):
1720
def setUp(self) -> None:
21+
self.client = Client()
22+
session = self.client.session
23+
session["altcha_verified_at"] = time.time()
24+
session.save()
1825
self.vulnerability = Vulnerability.objects.create(
1926
vulnerability_id="VCID-1234",
2027
summary="test-vuln1",

vulnerabilities/tests/test_view.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
class PackageSearchTestCase(TestCase):
3737
def setUp(self):
3838
self.client = Client()
39+
session = self.client.session
40+
session["altcha_verified_at"] = time.time()
41+
session.save()
3942
packages = [
4043
"pkg:nginx/nginx@0.6.18",
4144
"pkg:nginx/nginx@1.20.0",
@@ -169,6 +172,9 @@ def setUp(self):
169172
alias = Alias(alias="TEST-2022", vulnerability=vulnerability)
170173
alias.save()
171174
self.client = Client()
175+
session = self.client.session
176+
session["altcha_verified_at"] = time.time()
177+
session.save()
172178

173179
def test_vulnerabilties_search_view_with_vcid_works_and_pk_does_not(self):
174180
response = self.client.get(f"/vulnerabilities/{self.vulnerability.pk}")
@@ -323,20 +329,28 @@ def setUp(self):
323329

324330
self.vuln1.severities.add(self.severity1)
325331
self.vuln1.severities.add(self.severity2)
332+
self.client = Client()
333+
session = self.client.session
334+
session["altcha_verified_at"] = time.time()
335+
session.save()
326336
self.vuln1.save()
327337

328338
def test_aggregate_fixed_and_affected_packages(self):
329-
with self.assertNumQueries(11):
339+
with self.assertNumQueries(12):
330340
start_time = time.time()
331341
response = self.client.get(f"/vulnerabilities/{self.vuln1.vulnerability_id}")
332342
end_time = time.time()
333-
assert end_time - start_time < 0.05
343+
# Increase time for ALTCHA verification
344+
assert end_time - start_time < 0.06
334345
self.assertEqual(response.status_code, 200)
335346

336347

337348
class ThrottleTestCase(TestCase):
338349
def setUp(self):
339350
self.client = Client()
351+
session = self.client.session
352+
session["altcha_verified_at"] = time.time()
353+
session.save()
340354
cache.clear()
341355

342356
def test_throttle_after_15_requests(self):

vulnerabilities/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99

1010
import json
1111
import logging
12+
import time
1213
from collections import defaultdict
1314
from typing import List
1415

1516
from cvss.exceptions import CVSS2MalformedError
1617
from cvss.exceptions import CVSS3MalformedError
1718
from cvss.exceptions import CVSS4MalformedError
19+
from django import forms
1820
from django.contrib import messages
1921
from django.contrib.auth.views import LoginView
2022
from django.core.cache import cache
@@ -34,12 +36,15 @@
3436
from django.views import generic
3537
from django.views.generic.detail import DetailView
3638
from django.views.generic.edit import FormMixin
39+
from django.views.generic.edit import FormView
3740
from django.views.generic.list import ListView
41+
from django_altcha import AltchaField
3842

3943
from vulnerabilities import models
4044
from vulnerabilities.forms import AdminLoginForm
4145
from vulnerabilities.forms import AdvisorySearchForm
4246
from vulnerabilities.forms import AdvisoryToDoForm
47+
from vulnerabilities.forms import AltchaForm
4348
from vulnerabilities.forms import ApiUserCreationForm
4449
from vulnerabilities.forms import PackageSearchForm
4550
from vulnerabilities.forms import PipelineSchedulePackageForm
@@ -1154,3 +1159,12 @@ def get_context_data(self, **kwargs):
11541159
context["vulnerability_id"] = todo.alias
11551160
context["curation_items"] = json.dumps(todo.issue_detail["curation_items"])
11561161
return context
1162+
1163+
1164+
class AltchaView(FormView):
1165+
template_name = "altcha.html"
1166+
form_class = AltchaForm
1167+
1168+
def form_valid(self, form):
1169+
self.request.session["altcha_verified_at"] = time.time()
1170+
return redirect("/")

vulnerablecode/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
# SECURITY WARNING: do not run with debug turned on in production
4646
DEBUG = env.bool("VULNERABLECODE_DEBUG", default=False)
4747

48+
# remove altcha verification once broswer is closed
49+
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
50+
4851
# SECURITY WARNING: do not run with debug turned on in production
4952
DEBUG_TOOLBAR = env.bool("VULNERABLECODE_DEBUG_TOOLBAR", default=False)
5053

@@ -108,6 +111,7 @@
108111
"django.middleware.clickjacking.XFrameOptionsMiddleware",
109112
"vulnerabilities.middleware.ban_user_agent.BanUserAgent",
110113
"vulnerabilities.middleware.timezone.UserTimezoneMiddleware",
114+
"vulnerabilities.middleware.altcha_protection.AltchaProtectionMiddleware",
111115
)
112116

113117
ROOT_URLCONF = "vulnerablecode.urls"

vulnerablecode/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from vulnerabilities.views import AdvisoryPackagesDetails
3838
from vulnerabilities.views import AdvisoryToDoListView
3939
from vulnerabilities.views import AffectedByAdvisoriesListView
40+
from vulnerabilities.views import AltchaView
4041
from vulnerabilities.views import ApiUserCreateView
4142
from vulnerabilities.views import FixingAdvisoriesListView
4243
from vulnerabilities.views import HomePage
@@ -144,6 +145,7 @@ def __init__(self, *args, **kwargs):
144145
AdvisoryDetails.as_view(),
145146
name="advisory_details",
146147
),
148+
path("altcha/", AltchaView.as_view(), name="altcha"),
147149
path(
148150
"packages/search/",
149151
PackageSearch.as_view(),

0 commit comments

Comments
 (0)