From 8fa67b8671c7aea383ca6de9bded1b05e700a847 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 25 Aug 2022 14:10:39 +1000 Subject: [PATCH] Sanitize search text in bootstrap table (#3609) * Sanitize search text in bootstrap table * Clean search query on the server side before rendering search page template - Refactor existing sanitizing code into functions * Make ASCII and Unicode cleaning optional --- InvenTree/InvenTree/helpers.py | 51 +++++++++++++++++++++ InvenTree/InvenTree/mixins.py | 37 ++------------- InvenTree/InvenTree/views.py | 4 ++ InvenTree/templates/InvenTree/search.html | 34 +++++++------- InvenTree/templates/js/translated/tables.js | 4 +- 5 files changed, 80 insertions(+), 50 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index e1ea93c885..fcbe9f4c0e 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -20,7 +20,9 @@ from django.http import StreamingHttpResponse from django.test import TestCase from django.utils.translation import gettext_lazy as _ +import regex import requests +from bleach import clean from djmoney.money import Money from PIL import Image @@ -856,6 +858,55 @@ def clean_decimal(number): return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize() +def strip_html_tags(value: str, raise_error=True, field_name=None): + """Strip HTML tags from an input string using the bleach library. + + If raise_error is True, a ValidationError will be thrown if HTML tags are detected + """ + + cleaned = clean( + value, + strip=True, + tags=[], + attributes=[], + ) + + # Add escaped characters back in + replacements = { + '>': '>', + '<': '<', + '&': '&', + } + + for o, r in replacements.items(): + cleaned = cleaned.replace(o, r) + + # If the length changed, it means that HTML tags were removed! + if len(cleaned) != len(value) and raise_error: + + field = field_name or 'non_field_errors' + + raise ValidationError({ + field: [_("Remove HTML tags from this value")] + }) + + return cleaned + + +def remove_non_printable_characters(value: str, remove_ascii=True, remove_unicode=True): + """Remove non-printable / control characters from the provided string""" + + if remove_ascii: + # Remove ASCII control characters + cleaned = regex.sub(u'[\x01-\x1F]+', '', value) + + if remove_unicode: + # Remove Unicode control characters + cleaned = regex.sub(u'[^\P{C}]+', '', value) + + return cleaned + + def get_objectreference(obj, type_ref: str = 'content_type', object_ref: str = 'object_id'): """Lookup method for the GenericForeignKey fields. diff --git a/InvenTree/InvenTree/mixins.py b/InvenTree/InvenTree/mixins.py index a976eb2e00..5d81c26cb2 100644 --- a/InvenTree/InvenTree/mixins.py +++ b/InvenTree/InvenTree/mixins.py @@ -1,13 +1,10 @@ """Mixins for (API) views in the whole project.""" -from django.utils.translation import gettext_lazy as _ - -import regex -from bleach import clean from rest_framework import generics, status -from rest_framework.exceptions import ValidationError from rest_framework.response import Response +from InvenTree.helpers import remove_non_printable_characters, strip_html_tags + class CleanMixin(): """Model mixin class which cleans inputs using the Mozilla bleach tools.""" @@ -49,34 +46,8 @@ class CleanMixin(): Ref: https://github.com/mozilla/bleach/issues/192 """ - cleaned = clean( - data, - strip=True, - tags=[], - attributes=[], - ) - - # Add escaped characters back in - replacements = { - '>': '>', - '<': '<', - '&': '&', - } - - for o, r in replacements.items(): - cleaned = cleaned.replace(o, r) - - # If the length changed, it means that HTML tags were removed! - if len(cleaned) != len(data): - raise ValidationError({ - field: [_("Remove HTML tags from this value")] - }) - - # Remove ASCII control characters - cleaned = regex.sub(u'[\x01-\x1F]+', '', cleaned) - - # Remove Unicode control characters - cleaned = regex.sub(u'[^\P{C}]+', '', cleaned) + cleaned = strip_html_tags(data, field_name=field) + cleaned = remove_non_printable_characters(cleaned) return cleaned diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index ecb6085cb7..3c410bd1ca 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -38,6 +38,7 @@ from part.models import PartCategory from users.models import RuleSet, check_user_role from .forms import EditUserForm, SetPasswordForm +from .helpers import remove_non_printable_characters, strip_html_tags def auth_request(request): @@ -600,6 +601,9 @@ class SearchView(TemplateView): query = request.POST.get('search', '') + query = strip_html_tags(query, raise_error=False) + query = remove_non_printable_characters(query) + context['query'] = query return super(TemplateView, self).render_to_response(context) diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index eb22cc0889..4596fb9f7f 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -33,6 +33,8 @@ }); } + var search_text = sanitizeInputString("{{ query|escapejs }}"); + function addItem(label, title, icon, options) { // Construct a "badge" to add to the sidebar item @@ -85,7 +87,7 @@ "{% url 'api-part-list' %}", { params: { - original_search: "{{ query }}", + original_search: search_text, }, checkbox: false, disableFilters: true, @@ -96,7 +98,7 @@ loadPartCategoryTable($("#table-category"), { params: { - original_search: "{{ query }}", + original_search: search_text, } }); @@ -107,7 +109,7 @@ "{% url 'api-manufacturer-part-list' %}", { params: { - original_search: "{{ query }}", + original_search: search_text, part_detail: true, supplier_detail: true, manufacturer_detail: true @@ -122,7 +124,7 @@ "{% url 'api-supplier-part-list' %}", { params: { - original_search: "{{ query }}", + original_search: search_text, part_detail: true, supplier_detail: true, manufacturer_detail: true @@ -141,7 +143,7 @@ loadBuildTable('#table-build-order', { locale: '{{ request.LANGUAGE_CODE }}', params: { - original_search: '{{ query }}', + original_search: search_text, } }); @@ -156,7 +158,7 @@ filterKey: 'stocksearch', url: "{% url 'api-stock-list' %}", params: { - original_search: "{{ query }}", + original_search: search_text, part_detail: true, location_detail: true } @@ -167,7 +169,7 @@ loadStockLocationTable($("#table-location"), { filterKey: 'locationsearch', params: { - original_search: "{{ query }}", + original_search: search_text, }, }); @@ -180,8 +182,8 @@ loadCompanyTable('#table-manufacturer', "{% url 'api-company-list' %}", { params: { - original_search: "{{ query }}", - is_manufacturer: "true", + original_search: search_text, + is_manufacturer: true, } }); @@ -190,8 +192,8 @@ loadCompanyTable('#table-supplier', "{% url 'api-company-list' %}", { params: { - original_search: "{{ query }}", - is_supplier: "true", + original_search: search_text, + is_supplier: true, } }); @@ -199,7 +201,7 @@ loadPurchaseOrderTable('#table-purchase-order', { params: { - original_search: '{{ query }}', + original_search: search_text, } }); @@ -210,8 +212,8 @@ loadCompanyTable('#table-customer', "{% url 'api-company-list' %}", { params: { - original_search: "{{ query }}", - is_customer: "true", + original_search: search_text, + is_customer: true, } }); @@ -219,7 +221,7 @@ loadSalesOrderTable('#table-sales-orders', { params: { - original_search: '{{ query }}', + original_search: search_text, } }); @@ -230,7 +232,7 @@ enableSidebar( 'search', { - hide_toggle: 'true', + hide_toggle: true, } ); diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index befe967860..90e5f3f324 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -346,7 +346,9 @@ function convertQueryParameters(params, filters) { if ('original_search' in params) { var search = params['search'] || ''; - params['search'] = search + ' ' + params['original_search']; + var clean_search = sanitizeInputString(search + ' ' + params['original_search']); + + params['search'] = clean_search; delete params['original_search']; }