2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-07 03:50:52 +00:00

refactor(backend): replace bleach with nh3 and bump weasy (#11655)

* Replace bleach with nh3 for HTML sanitization

Agent-Logs-Url: https://github.com/matmair/InvenTree/sessions/913a447a-5efa-4fa3-b8b1-6af5feaa24f0

Co-authored-by: matmair <66015116+matmair@users.noreply.github.com>

* reduce diff

* bump weasy

* fix name

* remove old textual refs

* move defaults

* add some comments

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Matthias Mair
2026-04-02 06:35:15 +02:00
committed by GitHub
parent 07a0bd2e24
commit 5d1cbf4e9a
9 changed files with 265 additions and 79 deletions

View File

@@ -21,16 +21,19 @@ from django.http import StreamingHttpResponse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import bleach import nh3
import bleach.css_sanitizer
import bleach.sanitizer
import structlog import structlog
from bleach import clean
from djmoney.money import Money from djmoney.money import Money
from PIL import Image from PIL import Image
from stdimage.models import StdImageField, StdImageFieldFile from stdimage.models import StdImageField, StdImageFieldFile
from common.currency import currency_code_default from common.currency import currency_code_default
from InvenTree.sanitizer import (
DEAFAULT_ATTRS,
DEFAULT_CSS,
DEFAULT_PROTOCOLS,
DEFAULT_TAGS,
)
from .setting.storages import StorageBackends from .setting.storages import StorageBackends
from .settings import MEDIA_URL, STATIC_URL from .settings import MEDIA_URL, STATIC_URL
@@ -895,13 +898,13 @@ def clean_decimal(number):
def strip_html_tags(value: str, raise_error=True, field_name=None): def strip_html_tags(value: str, raise_error=True, field_name=None):
"""Strip HTML tags from an input string using the bleach library. """Strip HTML tags from an input string using the nh3 library.
If raise_error is True, a ValidationError will be thrown if HTML tags are detected If raise_error is True, a ValidationError will be thrown if HTML tags are detected
""" """
value = str(value).strip() value = str(value).strip()
cleaned = clean(value, strip=True, tags=[], attributes=[]) cleaned = nh3.clean(value, tags=frozenset())
# Add escaped characters back in # Add escaped characters back in
replacements = {'&gt;': '>', '&lt;': '<', '&amp;': '&'} replacements = {'&gt;': '>', '&lt;': '<', '&amp;': '&'}
@@ -961,34 +964,32 @@ def clean_markdown(value: str) -> str:
output_format='html', output_format='html',
) )
# Bleach settings # nh3 sanitizer settings
whitelist_tags = markdownify_settings.get( whitelist_tags = markdownify_settings.get('WHITELIST_TAGS', DEFAULT_TAGS)
'WHITELIST_TAGS', bleach.sanitizer.ALLOWED_TAGS whitelist_attrs = markdownify_settings.get('WHITELIST_ATTRS', DEAFAULT_ATTRS)
) whitelist_styles = markdownify_settings.get('WHITELIST_STYLES', DEFAULT_CSS)
whitelist_attrs = markdownify_settings.get(
'WHITELIST_ATTRS', bleach.sanitizer.ALLOWED_ATTRIBUTES
)
whitelist_styles = markdownify_settings.get(
'WHITELIST_STYLES', bleach.css_sanitizer.ALLOWED_CSS_PROPERTIES
)
whitelist_protocols = markdownify_settings.get( whitelist_protocols = markdownify_settings.get(
'WHITELIST_PROTOCOLS', bleach.sanitizer.ALLOWED_PROTOCOLS 'WHITELIST_PROTOCOLS', DEFAULT_PROTOCOLS
) )
strip = markdownify_settings.get('STRIP', True)
css_sanitizer = bleach.css_sanitizer.CSSSanitizer( # Convert bleach-style attributes (list or dict) to nh3-compatible dict format
allowed_css_properties=whitelist_styles if isinstance(whitelist_attrs, (list, tuple, set, frozenset)):
) attrs_dict = {'*': set(whitelist_attrs)}
cleaner = bleach.Cleaner( elif isinstance(whitelist_attrs, dict):
tags=whitelist_tags, attrs_dict = {tag: set(allowed) for tag, allowed in whitelist_attrs.items()}
attributes=whitelist_attrs, else:
css_sanitizer=css_sanitizer, attrs_dict = None
protocols=whitelist_protocols,
strip=strip,
)
# Clean the HTML content (for comparison). This must be the same as the original content # Clean the HTML content (for comparison). This must be the same as the original content
clean_html = cleaner.clean(html) clean_html = nh3.clean(
html,
tags=set(whitelist_tags),
attributes=attrs_dict,
url_schemes=set(whitelist_protocols),
filter_style_properties=set(whitelist_styles),
link_rel=None,
strip_comments=True,
)
if html != clean_html: if html != clean_html:
raise ValidationError(_('Data contains prohibited markdown content')) raise ValidationError(_('Data contains prohibited markdown content'))

View File

@@ -19,7 +19,7 @@ from InvenTree.serializers import FilterableSerializerMixin
class CleanMixin: class CleanMixin:
"""Model mixin class which cleans inputs using the Mozilla bleach tools.""" """Model mixin class which cleans inputs using nh3."""
# Define a list of field names which will *not* be cleaned # Define a list of field names which will *not* be cleaned
SAFE_FIELDS = [] SAFE_FIELDS = []
@@ -52,16 +52,7 @@ class CleanMixin:
return Response(serializer.data) return Response(serializer.data)
def clean_string(self, field: str, data: str) -> str: def clean_string(self, field: str, data: str) -> str:
"""Clean / sanitize a single input string. """Clean / sanitize a single input string."""
Note that this function will *allow* orphaned <>& characters,
which would normally be escaped by bleach.
Nominally, the only thing that will be "cleaned" will be HTML tags
Ref: https://github.com/mozilla/bleach/issues/192
"""
cleaned = data cleaned = data
# By default, newline characters are removed # By default, newline characters are removed
@@ -101,7 +92,7 @@ class CleanMixin:
def clean_data(self, data: dict) -> dict: def clean_data(self, data: dict) -> dict:
"""Clean / sanitize data. """Clean / sanitize data.
This uses Mozilla's bleach under the hood to disable certain html tags by This uses nh3 under the hood to disable certain html tags by
encoding them - this leads to script tags etc. to not work. encoding them - this leads to script tags etc. to not work.
The results can be longer then the input; might make some character combinations The results can be longer then the input; might make some character combinations
`ugly`. Prevents XSS on the server-level. `ugly`. Prevents XSS on the server-level.

View File

@@ -1,7 +1,66 @@
"""Functions to sanitize user input files.""" """Functions to sanitize user input files."""
from bleach import clean import nh3
from bleach.css_sanitizer import CSSSanitizer
# Allowed CSS properties for SVG sanitization (combines general CSS and SVG-specific properties)
_SVG_ALLOWED_CSS_PROPERTIES = frozenset([
# General CSS (matching bleach's original ALLOWED_CSS_PROPERTIES)
'azimuth',
'background-color',
'border-bottom-color',
'border-collapse',
'border-color',
'border-left-color',
'border-right-color',
'border-top-color',
'clear',
'color',
'cursor',
'direction',
'display',
'elevation',
'float',
'font',
'font-family',
'font-size',
'font-style',
'font-variant',
'font-weight',
'height',
'letter-spacing',
'line-height',
'overflow',
'pause',
'pause-after',
'pause-before',
'pitch',
'pitch-range',
'richness',
'speak',
'speak-header',
'speak-numeral',
'speak-punctuation',
'speech-rate',
'stress',
'text-align',
'text-decoration',
'text-indent',
'unicode-bidi',
'vertical-align',
'voice-family',
'volume',
'white-space',
'width',
# SVG-specific CSS (matching bleach's ALLOWED_SVG_PROPERTIES)
'fill',
'fill-opacity',
'fill-rule',
'stroke',
'stroke-linecap',
'stroke-linejoin',
'stroke-opacity',
'stroke-width',
])
ALLOWED_ELEMENTS_SVG = [ ALLOWED_ELEMENTS_SVG = [
'a', 'a',
@@ -184,6 +243,74 @@ ALLOWED_ATTRIBUTES_SVG = [
'style', 'style',
] ]
# Default allowlists (matching bleach's original defaults)
# TODO: I do not see us needing a bunch of these but I do not want to introduce a breaking change; we might want to narroy this down with the next breaking change
DEFAULT_TAGS = frozenset([
'a',
'abbr',
'acronym',
'b',
'blockquote',
'code',
'em',
'i',
'li',
'ol',
'strong',
'ul',
])
DEAFAULT_ATTRS = {'a': {'href', 'title'}, 'abbr': {'title'}, 'acronym': {'title'}}
DEFAULT_CSS = frozenset([
'azimuth',
'background-color',
'border-bottom-color',
'border-collapse',
'border-color',
'border-left-color',
'border-right-color',
'border-top-color',
'clear',
'color',
'cursor',
'direction',
'display',
'elevation',
'float',
'font',
'font-family',
'font-size',
'font-style',
'font-variant',
'font-weight',
'height',
'letter-spacing',
'line-height',
'overflow',
'pause',
'pause-after',
'pause-before',
'pitch',
'pitch-range',
'richness',
'speak',
'speak-header',
'speak-numeral',
'speak-punctuation',
'speech-rate',
'stress',
'text-align',
'text-decoration',
'text-indent',
'unicode-bidi',
'vertical-align',
'voice-family',
'volume',
'white-space',
'width',
])
# TODO: We might want to respect the setting EXTRA_URL_SCHEMES here but that would be breaking
DEFAULT_PROTOCOLS = frozenset(['http', 'https', 'mailto'])
def sanitize_svg( def sanitize_svg(
file_data, file_data,
@@ -206,13 +333,16 @@ def sanitize_svg(
if isinstance(file_data, bytes): if isinstance(file_data, bytes):
file_data = file_data.decode('utf-8') file_data = file_data.decode('utf-8')
cleaned = clean( # nh3 requires attributes as dict[str, set[str]]; convert from list (allowed for all elements)
attrs_dict = {elem: set(attributes) for elem in elements}
cleaned = nh3.clean(
file_data, file_data,
tags=elements, tags=set(elements),
attributes=attributes, attributes=attrs_dict,
strip=strip, filter_style_properties=_SVG_ALLOWED_CSS_PROPERTIES,
strip_comments=strip, strip_comments=strip,
css_sanitizer=CSSSanitizer(), link_rel=None,
) )
return cleaned return cleaned

View File

@@ -1604,11 +1604,11 @@ class SanitizerTest(TestCase):
def test_svg_sanitizer(self): def test_svg_sanitizer(self):
"""Test that SVGs are sanitized accordingly.""" """Test that SVGs are sanitized accordingly."""
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0} valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91;"></path> <path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91"></path>
</svg>""" </svg>"""
dangerous_string = valid_string.format('<script>alert();</script>') dangerous_string = valid_string.format('<script>alert();</script>')
# Test that valid string # Test that valid string passes through unchanged
self.assertEqual(valid_string, sanitize_svg(valid_string)) self.assertEqual(valid_string, sanitize_svg(valid_string))
# Test that invalid string is cleaned # Test that invalid string is cleaned

View File

@@ -276,7 +276,7 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
# There should not be any templates left at this point # There should not be any templates left at this point
self.assertEqual(PartCategoryParameterTemplate.objects.count(), 0) self.assertEqual(PartCategoryParameterTemplate.objects.count(), 0)
def test_bleach(self): def test_sanitizer(self):
"""Test that the data cleaning functionality is working. """Test that the data cleaning functionality is working.
This helps to protect against XSS injection This helps to protect against XSS injection

View File

@@ -33,7 +33,7 @@ def image_data(img, fmt='PNG') -> str:
def clean_barcode(data): def clean_barcode(data):
"""Return a 'cleaned' string for encoding into a barcode / qrcode. """Return a 'cleaned' string for encoding into a barcode / qrcode.
- This function runs the data through bleach, and removes any malicious HTML content. - This function sanitizes the data using nh3, and removes any malicious HTML content.
- Used to render raw barcode data into the rendered HTML templates - Used to render raw barcode data into the rendered HTML templates
""" """
from InvenTree.helpers import strip_html_tags from InvenTree.helpers import strip_html_tags

View File

@@ -89,9 +89,9 @@ bcrypt==5.0.0 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# paramiko # paramiko
bleach[css]==6.3.0 \ bleach==4.1.0 \
--hash=sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22 \ --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \
--hash=sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6 --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# django-markdownify # django-markdownify
@@ -631,9 +631,9 @@ django-maintenance-mode==0.22.0 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# -r src/backend/requirements.in # -r src/backend/requirements.in
django-markdownify==0.9.6 \ django-markdownify==0.9.1 \
--hash=sha256:9863b2bfa6d159ad1423dc93bf0d6eadc6413776de304049aa9fcfa5edd2ce1c \ --hash=sha256:06ff2994ff09ce030b50de8c6fc5b89b9c25a66796948aff55370716ca1233af \
--hash=sha256:edcf47b2026d55a8439049d35c8b54e11066a4856c4fad1060e139cb3d2eee52 --hash=sha256:24ba68b8a5996b6ec9632d11a3fd2e7159cb7e6becd3104e0a9372b5a2a148ef
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# -r src/backend/requirements.in # -r src/backend/requirements.in
@@ -1277,6 +1277,37 @@ markupsafe==3.0.3 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# jinja2 # jinja2
nh3==0.3.4 \
--hash=sha256:07999b998bf89692738f15c0eac76a416382932f855709e0b7488b595c30ec89 \
--hash=sha256:0961a27dc2057c38d0364cb05880e1997ae1c80220cbc847db63213720b8f304 \
--hash=sha256:0d825722a1e8cbc87d7ca1e47ffb1d2a6cf343ad4c1b8465becf7cadcabcdfd0 \
--hash=sha256:18a2e44ccb29cbb45071b8f3f2dab9ebfb41a6516f328f91f1f1fd18196239a4 \
--hash=sha256:3390e4333883673a684ce16c1716b481e91782d6f56dec5c85fed9feedb23382 \
--hash=sha256:41e46b3499918ab6128b6421677b316e79869d0c140da24069d220a94f4e72d1 \
--hash=sha256:43ad4eedee7e049b9069bc015b7b095d320ed6d167ecec111f877de1540656e9 \
--hash=sha256:47d749d99ae005ab19517224140b280dd56e77b33afb82f9b600e106d0458003 \
--hash=sha256:4aa8b43e68c26b68069a3b6cef09de166d1d7fa140cf8d77e409a46cbf742e44 \
--hash=sha256:554cc2bab281758e94d770c3fb0bf2d8be5fb403ef6b2e8841dd7c1615df7a0f \
--hash=sha256:72e4e9ca1c4bd41b4a28b0190edc2e21e3f71496acd36a0162858e1a28db3d7e \
--hash=sha256:75643c22f5092d8e209f766ee8108c400bc1e44760fc94d2d638eb138d18f853 \
--hash=sha256:7cae217f031809321db962cd7e092bda8d4e95a87f78c0226628fa6c2ea8ebc5 \
--hash=sha256:80b955d802bf365bd42e09f6c3d64567dce777d20e97968d94b3e9d9e99b265e \
--hash=sha256:87dac8d611b4a478400e0821a13b35770e88c266582f065e7249d6a37b0f86e8 \
--hash=sha256:883d5a6d6ee8078c4afc8e96e022fe579c4c265775ff6ee21e39b8c542cabab3 \
--hash=sha256:8b61058f34c2105d44d2a4d4241bacf603a1ef5c143b08766bbd0cf23830118f \
--hash=sha256:8d697e19f2995b337f648204848ac3a528eaafffc39e7ce4ac6b7a2fbe6c84af \
--hash=sha256:9337517edb7c10228252cce2898e20fb3d77e32ffaccbb3c66897927d74215a0 \
--hash=sha256:96709a379997c1b28c8974146ca660b0dcd3794f4f6d50c1ea549bab39ac6ade \
--hash=sha256:c10b1f0c741e257a5cb2978d6bac86e7c784ab20572724b20c6402c2e24bce75 \
--hash=sha256:ca90397c8d36c1535bf1988b2bed006597337843a164c7ec269dc8813f37536b \
--hash=sha256:d866701affe67a5171b916b5c076e767a74c6a9efb7fb2006eb8d3c5f9a293d5 \
--hash=sha256:d8bebcb20ab4b91858385cd98fe58046ec4a624275b45ef9b976475604f45b49 \
--hash=sha256:dbe76feaa44e2ef9436f345016012a591550e77818876a8de5c8bc2a248e08df \
--hash=sha256:f5f214618ad5eff4f2a6b13a8d4da4d9e7f37c569d90a13fb9f0caaf7d04fe21 \
--hash=sha256:f987cb56458323405e8e5ea827e1befcf141ffa0c0ac797d6d02e6b646056d9a
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
oauthlib==3.3.1 \ oauthlib==3.3.1 \
--hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \
--hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1
@@ -1447,6 +1478,7 @@ packaging==26.0 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# bleach
# gunicorn # gunicorn
# opentelemetry-instrumentation # opentelemetry-instrumentation
paramiko==4.0.0 \ paramiko==4.0.0 \
@@ -2086,6 +2118,7 @@ six==1.17.0 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# bleach
# python-dateutil # python-dateutil
sqlparse==0.5.5 \ sqlparse==0.5.5 \
--hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
@@ -2106,12 +2139,11 @@ tablib[xls, xlsx, yaml]==3.9.0 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# -r src/backend/requirements.in # -r src/backend/requirements.in
tinycss2==1.4.0 \ tinycss2==1.5.1 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# bleach
# cssselect2 # cssselect2
# weasyprint # weasyprint
tinyhtml5==2.1.0 \ tinyhtml5==2.1.0 \
@@ -2165,9 +2197,9 @@ wcwidth==0.6.0 \
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# blessed # blessed
# prettytable # prettytable
weasyprint==66.0 \ weasyprint==68.1 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \ --hash=sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be \
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40 --hash=sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# -r src/backend/requirements.in # -r src/backend/requirements.in

View File

@@ -37,6 +37,7 @@ drf-spectacular # DRF API documentation
feedparser # RSS newsfeed parser feedparser # RSS newsfeed parser
gunicorn # Gunicorn web server gunicorn # Gunicorn web server
jinja2 # Jinja2 templating engine jinja2 # Jinja2 templating engine
nh3 # HTML sanitization (replaces bleach)
pdf2image # PDF to image conversion pdf2image # PDF to image conversion
pillow # Image manipulation pillow # Image manipulation
pint # Unit conversion pint # Unit conversion

View File

@@ -87,9 +87,9 @@ bcrypt==5.0.0 \
--hash=sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822 \ --hash=sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822 \
--hash=sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b --hash=sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b
# via paramiko # via paramiko
bleach[css]==6.3.0 \ bleach==4.1.0 \
--hash=sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22 \ --hash=sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da \
--hash=sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6 --hash=sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994
# via django-markdownify # via django-markdownify
blessed==1.33.0 \ blessed==1.33.0 \
--hash=sha256:1bc8ecac6d139286ea51ec1683433528ce75b0c60db77b7d881112bf9fc85b0f \ --hash=sha256:1bc8ecac6d139286ea51ec1683433528ce75b0c60db77b7d881112bf9fc85b0f \
@@ -585,9 +585,9 @@ django-maintenance-mode==0.22.0 \
--hash=sha256:502f04f845d6996e8add321186b3b9236c3702de7cb0ab14952890af6523b9e5 \ --hash=sha256:502f04f845d6996e8add321186b3b9236c3702de7cb0ab14952890af6523b9e5 \
--hash=sha256:a9cf2ba79c9945bd67f98755a6cfd281869d39b3745bbb5d1f571d058657aa85 --hash=sha256:a9cf2ba79c9945bd67f98755a6cfd281869d39b3745bbb5d1f571d058657aa85
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
django-markdownify==0.9.6 \ django-markdownify==0.9.1 \
--hash=sha256:9863b2bfa6d159ad1423dc93bf0d6eadc6413776de304049aa9fcfa5edd2ce1c \ --hash=sha256:06ff2994ff09ce030b50de8c6fc5b89b9c25a66796948aff55370716ca1233af \
--hash=sha256:edcf47b2026d55a8439049d35c8b54e11066a4856c4fad1060e139cb3d2eee52 --hash=sha256:24ba68b8a5996b6ec9632d11a3fd2e7159cb7e6becd3104e0a9372b5a2a148ef
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
django-money==3.6.0 \ django-money==3.6.0 \
--hash=sha256:94402f2831f2726b94ef2da35b4059441b4c0aedfc47b312472200d4ffdf8d73 \ --hash=sha256:94402f2831f2726b94ef2da35b4059441b4c0aedfc47b312472200d4ffdf8d73 \
@@ -1145,6 +1145,35 @@ markupsafe==3.0.3 \
--hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \
--hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
# via jinja2 # via jinja2
nh3==0.3.4 \
--hash=sha256:07999b998bf89692738f15c0eac76a416382932f855709e0b7488b595c30ec89 \
--hash=sha256:0961a27dc2057c38d0364cb05880e1997ae1c80220cbc847db63213720b8f304 \
--hash=sha256:0d825722a1e8cbc87d7ca1e47ffb1d2a6cf343ad4c1b8465becf7cadcabcdfd0 \
--hash=sha256:18a2e44ccb29cbb45071b8f3f2dab9ebfb41a6516f328f91f1f1fd18196239a4 \
--hash=sha256:3390e4333883673a684ce16c1716b481e91782d6f56dec5c85fed9feedb23382 \
--hash=sha256:41e46b3499918ab6128b6421677b316e79869d0c140da24069d220a94f4e72d1 \
--hash=sha256:43ad4eedee7e049b9069bc015b7b095d320ed6d167ecec111f877de1540656e9 \
--hash=sha256:47d749d99ae005ab19517224140b280dd56e77b33afb82f9b600e106d0458003 \
--hash=sha256:4aa8b43e68c26b68069a3b6cef09de166d1d7fa140cf8d77e409a46cbf742e44 \
--hash=sha256:554cc2bab281758e94d770c3fb0bf2d8be5fb403ef6b2e8841dd7c1615df7a0f \
--hash=sha256:72e4e9ca1c4bd41b4a28b0190edc2e21e3f71496acd36a0162858e1a28db3d7e \
--hash=sha256:75643c22f5092d8e209f766ee8108c400bc1e44760fc94d2d638eb138d18f853 \
--hash=sha256:7cae217f031809321db962cd7e092bda8d4e95a87f78c0226628fa6c2ea8ebc5 \
--hash=sha256:80b955d802bf365bd42e09f6c3d64567dce777d20e97968d94b3e9d9e99b265e \
--hash=sha256:87dac8d611b4a478400e0821a13b35770e88c266582f065e7249d6a37b0f86e8 \
--hash=sha256:883d5a6d6ee8078c4afc8e96e022fe579c4c265775ff6ee21e39b8c542cabab3 \
--hash=sha256:8b61058f34c2105d44d2a4d4241bacf603a1ef5c143b08766bbd0cf23830118f \
--hash=sha256:8d697e19f2995b337f648204848ac3a528eaafffc39e7ce4ac6b7a2fbe6c84af \
--hash=sha256:9337517edb7c10228252cce2898e20fb3d77e32ffaccbb3c66897927d74215a0 \
--hash=sha256:96709a379997c1b28c8974146ca660b0dcd3794f4f6d50c1ea549bab39ac6ade \
--hash=sha256:c10b1f0c741e257a5cb2978d6bac86e7c784ab20572724b20c6402c2e24bce75 \
--hash=sha256:ca90397c8d36c1535bf1988b2bed006597337843a164c7ec269dc8813f37536b \
--hash=sha256:d866701affe67a5171b916b5c076e767a74c6a9efb7fb2006eb8d3c5f9a293d5 \
--hash=sha256:d8bebcb20ab4b91858385cd98fe58046ec4a624275b45ef9b976475604f45b49 \
--hash=sha256:dbe76feaa44e2ef9436f345016012a591550e77818876a8de5c8bc2a248e08df \
--hash=sha256:f5f214618ad5eff4f2a6b13a8d4da4d9e7f37c569d90a13fb9f0caaf7d04fe21 \
--hash=sha256:f987cb56458323405e8e5ea827e1befcf141ffa0c0ac797d6d02e6b646056d9a
# via -r src/backend/requirements.in
oauthlib==3.3.1 \ oauthlib==3.3.1 \
--hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \
--hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1
@@ -1282,6 +1311,7 @@ packaging==26.0 \
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \
--hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529
# via # via
# bleach
# gunicorn # gunicorn
# opentelemetry-instrumentation # opentelemetry-instrumentation
paramiko==4.0.0 \ paramiko==4.0.0 \
@@ -1859,7 +1889,9 @@ sgmllib3k==1.0.0 \
six==1.17.0 \ six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via python-dateutil # via
# bleach
# python-dateutil
sqlparse==0.5.5 \ sqlparse==0.5.5 \
--hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \ --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \
--hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e
@@ -1874,11 +1906,10 @@ tablib[xls, xlsx, yaml]==3.9.0 \
--hash=sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2 \ --hash=sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2 \
--hash=sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a --hash=sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
tinycss2==1.4.0 \ tinycss2==1.5.1 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
# via # via
# bleach
# cssselect2 # cssselect2
# weasyprint # weasyprint
tinyhtml5==2.1.0 \ tinyhtml5==2.1.0 \
@@ -1926,9 +1957,9 @@ wcwidth==0.6.0 \
# via # via
# blessed # blessed
# prettytable # prettytable
weasyprint==66.0 \ weasyprint==68.1 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \ --hash=sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be \
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40 --hash=sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
webencodings==0.5.1 \ webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \