-
Notifications
You must be signed in to change notification settings - Fork 50
Upgrade back-end code from Django 4.2 to 5.2 LTS #8052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3b0b36e
eef1949
ed2cf1d
0033c8d
530e6a9
02206b4
0f3c25a
ffecf67
aed5838
686e165
31c2acb
6ef25c4
e2b0eee
a65c822
b602879
82ee2fe
a963629
15c82a2
3246bbd
614994b
ac18076
1dc34ca
435c163
219e60c
6a6d62d
6729ea7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| [pytest] | ||
| DJANGO_SETTINGS_MODULE = specifyweb.settings |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,12 @@ | ||
| django-stubs==1.12.0 | ||
| # django-stubs==1.12.0 | ||
| django-stubs==5.2.9 | ||
| mypy==1.5.1 | ||
| hypothesis>=6.131.9,<7 | ||
| hypothesis-jsonschema==0.19.0 | ||
| types-requests==2.28.5 | ||
| lxml | ||
| lxml | ||
| debugpy==1.6.5 | ||
| pytest==7.2.1 | ||
| pytest-django==4.12.0 | ||
| django-extensions==4.1 | ||
| # django-debug-toolbar==4.4.6 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,14 +5,14 @@ wheel | |
| kombu==5.5.2 | ||
| redis==6.4.0 | ||
| celery==5.5.1 | ||
| Django==4.2.30 | ||
| Django==5.2.13 | ||
| mysqlclient==2.1.1 | ||
| SQLAlchemy==1.4.54 | ||
| sqlalchemy2-stubs | ||
| requests==2.32.4 | ||
| pycryptodome==3.21.0 | ||
| PyJWT==2.12.1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== requirements.txt =="
sed -n '1,60p' requirements.txt 2>/dev/null || true
echo
echo "== PyJWT references in repo =="
rg -n --fixed-strings "PyJWKClient|PyJWT|requests_jwt|jwt\." -S . --glob '!*.lock' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -200 || true
echo
echo "== lockfiles mentioning PyJWT =="
for f in $(git ls-files | rg '(^|/)(requirements.*\.txt|poetry\.lock|Pipfile\.lock|uv\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$'); do
echo "--- $f"
rg -n --fixed-strings "PyJWT|pyjwt|jwk|jwk-set" "$f" || true
doneRepository: specify/specify7 Length of output: 673 🌐 Web query:
💡 Result: The vulnerability GHSA-993g-76c3-p5m4 (also tracked as CVE-2026-48522) involves a missing scheme allowlist in PyJWT's PyJWKClient, which allowed the library to fetch URIs using schemes beyond the expected http and https (such as file://, ftp://, and data:) [1][2][3]. This flaw enabled potential Server-Side Request Forgery (SSRF) and, in specific configurations, token forgery [2][4]. The vulnerability is fixed in PyJWT version 2.13.0 [3][5]. In this release, PyJWKClient was updated to strictly reject any URI scheme that is not explicitly http or https [5][6]. Additionally, a new allowed_schemes initialization parameter was added to allow for future configuration of allowed schemes [1][2]. Citations:
🌐 Web query:
💡 Result: The vulnerability GHSA-993g-76c3-p5m4, also identified as CVE-2026-48522, addressed a security flaw in PyJWT's PyJWKClient where it lacked a scheme allowlist [1][2]. This allowed the client to fetch JWKS data using unintended schemes such as file://, ftp://, and data:, potentially leading to SSRF or token forgery if an application accepted attacker-influenced URIs [1][3]. This issue was fixed in PyJWT version 2.13.0 [4][5]. The fix introduced an allowed_schemes parameter to the PyJWKClient constructor [6][7]. Key details of the fix: - PyJWKClient now defaults to allowed_schemes=("http", "https") [7]. - The library performs pre-validation of the URI scheme before invoking urllib.request.urlopen [1][7]. - If a scheme is not in the allowed list, PyJWKClient raises a PyJWKClientError [1][7]. - This change allows developers to maintain backward compatibility for standard HTTP/HTTPS use cases while providing the flexibility to enable other schemes explicitly if required [6][7]. It is important to note that this update specifically addresses the URI scheme restriction [1][5]. It is distinct from another security fix, GHSA-jq35-7prp-9v3f, which was also released in PyJWT 2.13.0 and addressed an algorithm allow-list bypass when using PyJWK keys [8][9]. Citations:
Upgrade PyJWT to avoid known PyJWKClient vulnerabilities.
🧰 Tools🪛 OSV Scanner (2.4.0)[HIGH] 14-14: pyjwt 2.12.1: undefined (PYSEC-2026-175) [HIGH] 14-14: pyjwt 2.12.1: undefined (PYSEC-2026-177) [HIGH] 14-14: pyjwt 2.12.1: undefined (PYSEC-2026-178) [HIGH] 14-14: pyjwt 2.12.1: undefined (PYSEC-2026-179) [HIGH] 14-14: pyjwt 2.12.1: PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes [HIGH] 14-14: pyjwt 2.12.1: PyJWKClient unbounded JWKS endpoint requests via attacker-controlled kid values (DoS) [HIGH] 14-14: pyjwt 2.12.1: PyJWT: Algorithm allow-list bypass when decoding with [HIGH] 14-14: pyjwt 2.12.1: PyJWT: Unauthenticated DoS via unbounded Base64URL decoding of unused payload segment in b64=false detached JWS [HIGH] 14-14: pyjwt 2.12.1: PyJWT: Public-key JWK accepted as HMAC secret enables forged HS256 tokens when mixed families are allowed 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| django-auth-ldap==1.2.17 | ||
| django-auth-ldap==5.3.0 | ||
| jsonschema==4.0.1 | ||
| typing-extensions==4.12.2 | ||
| stream-zip==0.0.83 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| from django.urls import re_path | ||
| from django.urls import path | ||
|
|
||
| from . import views | ||
|
|
||
| urlpatterns = [ | ||
| re_path(r'^batch_identify/resolve/$', views.batch_identify_resolve), | ||
| re_path(r'^batch_identify/validate_record_set/$', views.batch_identify_validate_record_set), | ||
| re_path(r'^batch_identify/$', views.batch_identify), | ||
| path('batch_identify/resolve/', views.batch_identify_resolve), | ||
| path('batch_identify/validate_record_set/', views.batch_identify_validate_record_set), | ||
| path('batch_identify/', views.batch_identify), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,15 +2,15 @@ | |||||
| Provides urls to access the mocked views | ||||||
| """ | ||||||
|
|
||||||
| from django.urls import path, re_path | ||||||
| from django.urls import path | ||||||
|
|
||||||
| from . import testsviews as views | ||||||
|
|
||||||
| urlpatterns = [ | ||||||
| path('collection/', views.collection), | ||||||
| re_path(r'^domain.json$', views.domain), | ||||||
| re_path(r'^viewsets/(?P<level>\d+).xml$', views.viewsets), | ||||||
| re_path(r'^schema_localization.json$', views.schema_localization), | ||||||
| re_path(r'^app.resource$', views.app_resource), | ||||||
| re_path(r'^available_related_searches.json$', views.available_related_searches), | ||||||
| path('domain.json', views.domain), | ||||||
| path('viewsets/<int:level>.xml', views.viewsets), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Files matching testurls.py:\n'
fd 'testurls\.py$' . || true
printf '\nGit-tracked context testurls.py:\n'
git ls-files | rg '(^|/)testurls\.py$' | sed -n '1,50p'
printf '\nRelevant snippets in context testurls.py:\n'
while IFS= read -r f; do
echo "--- $f"
nl -ba "$f" | sed -n '1,40p'
done < <(git ls-files | rg 'context/testurls\.py$' || true)
printf '\nSearch for viewsets and level=':
rg -n "def viewsets|viewsets|'viewset_'\+level|viewset_|level\.xml" --glob '*.py' .Repository: specify/specify7 Length of output: 392 🏁 Script executed: #!/bin/bash
set -u
echo "### testurls.py"
nl -ba specifyweb/backend/context/testurls.py | sed -n '1,80p'
echo
echo "### viewsets usages / level handling"
rg -n "def viewsets|viewsets|'viewset_'|\"viewset_|level\.xml|viewset_" --glob '*.py' .
echo
echo "### Django int converter behavior probe"
python3 - <<'PY'
try:
from django.urls.converters import IntConverter
ic = IntConverter()
converted = ic.to_python('05')
print(type(converted).__name__, repr(converted))
print(type(converted).__name__, type('').__name__, isinstance('', str))
print(converted + '/abc.xml')
except Exception as e:
print(type(e).__name__, str(e))
PYRepository: specify/specify7 Length of output: 3369 🏁 Script executed: #!/bin/bash
set -u
echo "### specifyweb/backend/context/testurls.py"
awk '{printf "%6d\t%s\n", NR, $0}' specifyweb/backend/context/testurls.py | sed -n '1,80p'
echo
echo "### specifyweb/backend/context/testsviews.py"
awk '{printf "%6d\t%s\n", NR, $0}' specifyweb/backend/context/testsviews.py | sed -n '1,60p'
echo
echo "### viewsets path/url/import references"
rg -n "testurls|testsviews|viewsets/<int:level>|def viewsets\(request, level\)" --glob '*.py' .Repository: specify/specify7 Length of output: 2308 🌐 Web query:
💡 Result: In Django, the int URL converter is used within path patterns to match a sequence of digits and convert the captured value into a Python integer [1][2]. Key technical details: - Matching behavior: The int converter matches zero or any positive integer [1][2]. - Type conversion: It automatically converts the captured string value into a Python int before passing it as a keyword argument to the view function [1][3]. - Implementation: Internally, it uses the regular expression [0-9]+ to identify matching segments [4]. - Usage: You use it in a URL pattern by placing it inside angle brackets, such as path('articles/int:year/', views.year_archive) [1][2]. If you need different matching logic (for example, to support negative numbers or specific constraints), you can register a custom path converter using the register_converter function [1][2][3]. Citations:
Keep Django’s Proposed fix- path('viewsets/<int:level>.xml', views.viewsets),
+ path('viewsets/<str:level>.xml', views.viewsets),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| path('schema_localization.json', views.schema_localization), | ||||||
| path('app.resource', views.app_resource), | ||||||
| path('available_related_searches.json', views.available_related_searches), | ||||||
| ] | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,12 @@ | ||
|
|
||
| from django.urls import include, path, re_path | ||
| from django.urls import path | ||
|
|
||
| from specifyweb.backend.inheritance import views | ||
|
|
||
| urlpatterns = [ | ||
| # cat num for siblings | ||
| re_path(r'^catalog_number_for_sibling/$', views.catalog_number_for_sibling), | ||
| path('catalog_number_for_sibling/', views.catalog_number_for_sibling), | ||
|
|
||
| # cat num for parent | ||
| re_path(r'^catalog_number_from_parent/$', views.catalog_number_from_parent), | ||
| ] | ||
| path('catalog_number_from_parent/', views.catalog_number_from_parent), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import React from 'react'; | ||
| import { ajax } from '../../utils/ajax'; | ||
| import { LoadingScreen } from '../Molecules/Dialog'; | ||
| import { formatUrl } from '../Router/queryString'; | ||
| import { LoadingContext } from '../Core/Contexts'; | ||
| import { softError } from '../Errors/assert'; | ||
|
|
||
| export function Logout() : JSX.Element { | ||
| const loading = React.useContext(LoadingContext); | ||
|
|
||
| loading( | ||
| ajax<string>('/accounts/logout/', { | ||
| method: 'POST', | ||
| headers: { }, | ||
| }) | ||
| .then( | ||
| () => globalThis.location.assign(formatUrl("/accounts/login/", {next: "/specify/"})), | ||
|
g1rly-c0d3r marked this conversation as resolved.
|
||
| (error) => softError(error) | ||
| ) | ||
| ) | ||
|
Comment on lines
+11
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'index\.tsx$' . | sed 's#^\./##' | grep 'frontend/js_src/lib/components/Logout/index.tsx' || true
echo "== file contents =="
cat -n specifyweb/frontend/js_src/lib/components/Logout/index.tsx
echo "== definitions/usages =="
rg -n "function Loading|const Loading|export .*Loading|function ajax|const ajax|function softError|const softError|loading\\(" specifyweb/frontend/js_src/lib/components specifyweb/frontend/js_src/lib 2>/dev/null | head -200Repository: specify/specify7 Length of output: 22738 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== candidate files for LoadingContext/loading/softError/Route components =="
rg -n "LoadingContext|LoadingScreen|loading\\(|softError|formatUrl\\(" specifyweb/frontend/js_src/lib -S
echo "== structural scan for Loading component definition =="
ast-grep scan --lang tsx --rule 'kind: Component; matcher: {kind: FunctionDeclaration, has: {kind: BlockStatement}}' specifyweb/frontend/js_src/lib/components 2>/dev/null || trueRepository: specify/specify7 Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== LoadingContext implementation =="
sed -n '1,230p' specifyweb/frontend/js_src/lib/components/Core/Contexts.tsx
echo "== ajax implementation relevant section =="
sed -n '1,155p' specifyweb/frontend/js_src/lib/utils/ajax/index.ts
echo "== static checker for render-time loading calls =="
python3 - <<'PY'
from pathlib import Path
p = Path('specifyweb/frontend/js_src/lib/components/Logout/index.tsx')
text = p.read_text()
print('contains ajax call:', 'ajax<string>' in text)
print('inside function body:')
start = text.index('export function Logout')
end = text.index('return <LoadingScreen />') + len('return <LoadingScreen />')
body = text[start:end]
print(body)
print('has useEffect:', 'useEffect' in body)
PYRepository: specify/specify7 Length of output: 14011 Move the logout request into an effect.
🤖 Prompt for AI Agents |
||
|
|
||
| return <LoadingScreen /> | ||
|
Comment on lines
+16
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Handle failures without leaving an endless loading screen.
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Generated by Django 5.2.13 on 2026-04-30 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ('specify', '0044_alter_deletion_cascade'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name='autonumberingscheme', | ||
| name='collections', | ||
| field=models.ManyToManyField(related_name='numberingschemes', through='specify.Autonumschcoll', through_fields=('autonumberingscheme', 'collection'), to='specify.collection'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='autonumberingscheme', | ||
| name='disciplines', | ||
| field=models.ManyToManyField(related_name='numberingschemes', through='specify.Autonumschdsp', through_fields=('autonumberingscheme', 'discipline'), to='specify.discipline'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='autonumberingscheme', | ||
| name='divisions', | ||
| field=models.ManyToManyField(related_name='numberingschemes', through='specify.Autonumschdiv', through_fields=('autonumberingscheme', 'division'), to='specify.division'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='project', | ||
| name='collectionobjects', | ||
| field=models.ManyToManyField(related_name='projects', through='specify.Project_colobj', through_fields=('project', 'collectionobject'), to='specify.collectionobject'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='specifyuser', | ||
| name='spprincipals', | ||
| field=models.ManyToManyField(related_name='specifyusers', through='specify.Specifyuser_spprincipal', through_fields=('specifyuser', 'spprincipal'), to='specify.spprincipal'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='spexportschema', | ||
| name='mappings', | ||
| field=models.ManyToManyField(related_name='spexportschemas', through='specify.Spexportschema_exportmapping', through_fields=('spexportschema', 'spexportschemamapping'), to='specify.spexportschemamapping'), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name='spprincipal', | ||
| name='sppermissions', | ||
| field=models.ManyToManyField(related_name='spprincipals', through='specify.Spprincipal_sppermission', through_fields=('spprincipal', 'sppermission'), to='specify.sppermission'), | ||
| ), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: specify/specify7
Length of output: 792
🌐 Web query:
django-stubs 5.2.9 mypy compatibility supported versions💡 Result:
For django-stubs version 5.2.9, the supported environment configurations are as follows [1][2]: Mypy versions: 1.13 through 1.19 [1][2] Django versions: 5.2 (full support), with partial support for Django 5.1 and 5.0 [1][2] Python versions: 3.10 through 3.13 [1][2] Version 5.2.9 was the final release of the 5.2.x series for django-stubs, designed to target Django 5.2 [3][4]. Subsequent releases transitioned to supporting Django 6.0 [3][4]. When using "partial support" for older Django versions, the stubs may contain newer type definitions (e.g., if a return type changed between versions) or include symbols not present in your specific, older Django installation [5]. It is generally recommended to use the version of django-stubs that corresponds to your installed Django version to ensure full compatibility [5].
Citations:
Align mypy with django-stubs 5.2.9.
django-stubs 5.2.9 documents supported mypy versions as 1.13–1.19, but this file pins
mypy==1.5.1. Use a supported mypy pin here, such asmypy==1.13.0, or keep the older django-stubs version if mypy 1.5.1 is required.🤖 Prompt for AI Agents