diff --git a/docs/docs/concepts/terminology.md b/docs/docs/concepts/terminology.md index ba127d9a48..95df1719ae 100644 --- a/docs/docs/concepts/terminology.md +++ b/docs/docs/concepts/terminology.md @@ -13,7 +13,7 @@ Evolves around manufacturing of parts out of other parts. It keeps track of stoc ### Part Library Management *(PLM)* Keeps track of BOMs, part variants, possible substitutions, versions, IPNs and further part parameters. -PLM can also mean product lifecycle management – those systems manage all stages from design through manufacturing up to customer support and recycling. +PLM can also mean product lifecycle management - those systems manage all stages from design through manufacturing up to customer support and recycling. A similar system is [Partkeepr](https://partkeepr.org/) (seems mostly inactive - there is a 3rd party importer). diff --git a/docs/docs/plugins/develop.md b/docs/docs/plugins/develop.md index fb6bfbec57..77b9a17975 100644 --- a/docs/docs/plugins/develop.md +++ b/docs/docs/plugins/develop.md @@ -23,7 +23,7 @@ Consider the use-case for your plugin and define the exact function of the plugi - Do you need to run in the background ([ScheduleMixin](./mixins/schedule.md)) or when things in InvenTree change ([EventMixin](./mixins/event.md))? - Does the plugin need configuration that should be user changeable ([SettingsMixin](./mixins/settings.md)) or static (just use a yaml in the config dir)? - You want to receive webhooks? Do not code your own untested function, use the WebhookEndpoint model as a base and override the perform_action method. -- Do you need the full power of Django with custom models and all the complexity that comes with that – welcome to the danger zone and [AppMixin](./mixins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance. +- Do you need the full power of Django with custom models and all the complexity that comes with that - welcome to the danger zone and [AppMixin](./mixins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance. ### Define Metadata diff --git a/src/backend/InvenTree/common/test_api.py b/src/backend/InvenTree/common/test_api.py index ac82ed8cc9..b0740c1b68 100644 --- a/src/backend/InvenTree/common/test_api.py +++ b/src/backend/InvenTree/common/test_api.py @@ -890,6 +890,364 @@ class NoteAPITests(InvenTreeAPITestCase): self.assertTrue(note_a_detail.data['primary']) +class NoteContentSanitizationTests(InvenTreeAPITestCase): + """Security tests for the Note API 'content' field. + + The content field accepts raw HTML which is sanitized by nh3 before + persistence. These tests verify that known XSS vectors are neutralised + both at the model level (Note.clean()) and through the API (POST/PATCH). + """ + + def setUp(self): + """Create a Part instance to attach notes to.""" + from part.models import Part + + super().setUp() + + self.assignRole('part.add') + + self.part = Part.objects.create( + name='Security Test Part', description='Part for note security testing' + ) + + def _note_url(self, pk=None): + if pk: + return reverse('api-note-detail', kwargs={'pk': pk}) + return reverse('api-note-list') + + def _create_note_with_content(self, content, expected_code=201): + return self.post( + self._note_url(), + data={ + 'model_type': 'part', + 'model_id': self.part.pk, + 'title': 'Security Test Note', + 'content': content, + }, + expected_code=expected_code, + ) + + # ------------------------------------------------------------------------- + # Model-level sanitization (Note.clean() called directly) + # ------------------------------------------------------------------------- + + def test_model_clean_strips_script_tags(self): + """Note.clean() removes
Safe content
", + ) + note.clean() + self.assertNotIn('hello
" + ) + content = response.data['content'] + self.assertNotIn('") + self.assertNotIn('") + self.assertNotIn('">' + ) + content = response.data['content'] + self.assertNotIn('Updated
"}, + expected_code=200, + ) + content = response.data['content'] + self.assertNotIn('