Moderate severityNVD Advisory· Published Dec 1, 2021· Updated Apr 30, 2025
CKAN - Stored Cross-Site Scripting (XSS) via SVG File Upload
CVE-2021-25967
Description
In CKAN, versions 2.9.0 to 2.9.3 are affected by a stored XSS vulnerability via SVG file upload of users’ profile picture. This allows low privileged application users to store malicious scripts in their profile picture. These scripts are executed in a victim’s browser when they open the malicious profile picture
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ckanPyPI | >= 2.9.0, < 2.10.0 | 2.10.0 |
Affected products
1Patches
15a46989c0a4fAllow strict types for user/group uploads
4 files changed · +80 −2
ckan/lib/uploader.py+21 −1 modified@@ -13,7 +13,7 @@ import ckan.lib.munge as munge import ckan.logic as logic import ckan.plugins as plugins -from ckan.common import config +from ckan.common import config, aslist ALLOWED_UPLOAD_TYPES = (cgi.FieldStorage, FlaskFileStorage) MB = 1 << 20 @@ -192,6 +192,26 @@ def upload(self, max_size=2): except OSError: pass + def verify_type(self): + if not self.filename: + return + + actual = magic.from_buffer(self.upload_file.read(1024), mime=True) + self.upload_file.seek(0, os.SEEK_SET) + + err = {self.file_field: [f"Unsupported upload type: {actual}"]} + + mimetypes = aslist( + config.get(f"ckan.upload.{self.object_type}.mimetypes")) + if mimetypes and actual not in mimetypes: + raise logic.ValidationError(err) + + type_ = actual.split("/")[0] + types = aslist( + config.get(f"ckan.upload.{self.object_type}.types")) + if types and type_ not in types: + raise logic.ValidationError(err) + class ResourceUpload(object): def __init__(self, resource):
ckan/logic/action/create.py+6 −0 modified@@ -762,6 +762,9 @@ def _group_or_org_create(context, data_dict, is_org=False): } logic.get_action('activity_create')(activity_create_context, activity_dict) + if hasattr(upload, "verify_type"): + upload.verify_type() + upload.upload(uploader.get_max_image_size()) if not context.get('defer_commit'): @@ -1012,6 +1015,9 @@ def user_create(context, data_dict): } logic.get_action('activity_create')(activity_create_context, activity_dict) + if hasattr(upload, "verify_type"): + upload.verify_type() + upload.upload(uploader.get_max_image_size()) if not context.get('defer_commit'):
ckan/tests/logic/action/test_create.py+51 −1 modified@@ -1666,7 +1666,7 @@ def test_ignored_on_create_if_non_sysadmin(self): @pytest.mark.usefixtures("clean_db") class TestUserImageUrl(object): - def test_upload_picture(self): + def test_external_picture(self): params = { "name": "test_user", @@ -1682,6 +1682,56 @@ def test_upload_picture(self): user_dict["image_display_url"] == "https://example.com/mypic.png" ) + def test_upload_non_picture_works_without_extra_config( + self, create_with_upload, faker): + params = { + "name": faker.user_name(), + "email": faker.email(), + "password": "12345678", + "action": "user_create", + "upload_field_name": "image_upload", + } + assert create_with_upload("hello world", "file.txt", **params) + + @pytest.mark.ckan_config("ckan.upload.user.types", "image") + def test_upload_non_picture(self, create_with_upload, faker): + params = { + "name": faker.user_name(), + "email": faker.email(), + "password": "12345678", + "action": "user_create", + "upload_field_name": "image_upload", + } + with pytest.raises( + logic.ValidationError, match="Unsupported upload type"): + create_with_upload("hello world", "file.txt", **params) + + @pytest.mark.ckan_config("ckan.upload.user.types", "image") + def test_upload_non_picture_with_png_extension( + self, create_with_upload, faker): + params = { + "name": faker.user_name(), + "email": faker.email(), + "password": "12345678", + "action": "user_create", + "upload_field_name": "image_upload", + } + with pytest.raises( + logic.ValidationError, match="Unsupported upload type"): + create_with_upload("hello world", "file.png", **params) + + @pytest.mark.ckan_config("ckan.upload.user.types", "image") + def test_upload_picture(self, create_with_upload, faker): + params = { + "name": faker.user_name(), + "email": faker.email(), + "password": "12345678", + "action": "user_create", + "upload_field_name": "image_upload", + } + assert create_with_upload(faker.image(), "file.png", **params) + + class TestVocabularyCreate(object): @pytest.mark.usefixtures("clean_db")
dev-requirements.txt+2 −0 modified@@ -4,12 +4,14 @@ beautifulsoup4==4.9.3 cookiecutter==1.7.3 coveralls #Let Unpinned - Requires latest coveralls docutils==0.16 +Faker==9.3.1 factory-boy==3.2.0 flask-debugtoolbar==0.11.0 freezegun==1.1.0 ipdb==0.13.7 pip-tools==5.1.2 pycodestyle==2.5.0 +Pillow==8.4.0 responses==0.13.3 sphinx-rtd-theme==0.4.3 sphinx==1.8.5
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-6w9p-88qg-p3g3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-25967ghsaADVISORY
- github.com/ckan/ckan/commit/5a46989c0a4f2c2873ca182c196da83b82babd25ghsaWEB
- github.com/ckan/ckan/pull/6477ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/ckan/PYSEC-2021-841.yamlghsaWEB
- www.whitesourcesoftware.com/vulnerability-database/CVE-2021-25967ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.