Skip to content

Commit

Permalink
Merge pull request #5121 from open-formulieren/issue/html-in-componen…
Browse files Browse the repository at this point in the history
…t-tooltip

Sanitizing HTML from component label, tooltip and description
  • Loading branch information
sergei-maertens authored Mar 10, 2025
2 parents 6169777 + 1d1a642 commit 4cb4b65
Show file tree
Hide file tree
Showing 10 changed files with 467 additions and 17 deletions.
39 changes: 34 additions & 5 deletions bin/report_component_problems.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
from __future__ import annotations

import re
import sys
from collections.abc import Sequence
from pathlib import Path
Expand Down Expand Up @@ -68,7 +69,23 @@ def check_component(component: Component) -> str | None:
return "defaultValue has a translation"


def report_problems(component_types: Sequence[str]) -> bool:
def check_component_html_usage(component: Component) -> list[str]:
messages = []
component_properties_to_check = ("label", "description", "tooltip")

for property_name in component_properties_to_check:
if property_name not in component:
continue

property_value = component[property_name]
property_contains_html = bool(re.search(r"<\w", property_value))
if property_contains_html:
messages.append(f"Component {property_name} contains html: '{property_value}'.")

return messages


def report_problems(component_types: Sequence[str], check_html_usage: bool) -> bool:
from openforms.forms.models import FormDefinition

problems = []
Expand All @@ -79,16 +96,21 @@ def report_problems(component_types: Sequence[str]) -> bool:
if component_types and component["type"] not in component_types:
continue

message = check_component(component)
if message is None:
messages = []
if check_component_message := check_component(component):
messages.append(check_component_message)
if check_html_usage:
messages.extend(check_component_html_usage(component))

if len(messages) == 0:
continue

problems.append(
[
form_definition.admin_name,
component.get("label") or component["key"],
component["type"],
message,
"\n".join(messages),
]
)

Expand Down Expand Up @@ -120,9 +142,16 @@ def main(skip_setup=False, **kwargs) -> bool:

@click.command()
@click.option("--component-type", multiple=True, help="Limit check to component type.")
def cli(component_type: Sequence[str]):
@click.option(
"--include-html-checking",
is_flag=True,
default=False,
help="Include checks for html usage in tooltips, descriptions and labels.",
)
def cli(component_type: Sequence[str], include_html_checking: bool):
return main(
component_types=component_type,
check_html_usage=include_html_checking,
)


Expand Down
46 changes: 40 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"copy-to-clipboard": "^3.3.1",
"design-token-editor": "^0.6.0",
"django-cookie-consent": "^0.6.0",
"dompurify": "^3.2.4",
"feelin": "^3.1.0",
"flatpickr": "^4.6.9",
"formik": "^2.2.9",
Expand Down
2 changes: 2 additions & 0 deletions src/openforms/formio/typing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ class Component(TypedDict):
key: str
label: str
multiple: NotRequired[bool]
tooltip: NotRequired[str]
description: NotRequired[str]
hidden: NotRequired[bool]
defaultValue: NotRequired[JSONValue]
validate: NotRequired[Validate]
Expand Down
16 changes: 16 additions & 0 deletions src/openforms/forms/api/serializers/form_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

from openforms.api.serializers import PublicFieldsSerializerMixin
from openforms.formio.service import rewrite_formio_components_for_request
from openforms.formio.utils import iter_components
from openforms.translations.api.serializers import ModelTranslationsSerializer

from ...models import Form, FormDefinition
from ...sanitizer import sanitize_component
from ...validators import (
validate_form_definition_is_reusable,
validate_no_duplicate_keys,
Expand Down Expand Up @@ -131,6 +133,20 @@ def to_representation(self, instance):

return representation

def create(self, validated_data):
if configuration := validated_data.get("configuration"):
for component in iter_components(configuration):
sanitize_component(component)

return super().create(validated_data)

def update(self, instance, validated_data):
if configuration := validated_data.get("configuration"):
for component in iter_components(configuration):
sanitize_component(component)

return super().update(instance, validated_data)

def validate(self, attrs):
attrs = super().validate(attrs)
if self.instance:
Expand Down
20 changes: 20 additions & 0 deletions src/openforms/forms/sanitizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from openforms.formio.typing import Component
from openforms.utils.sanitizer import sanitize_html_content


def sanitize_component(component: Component):
"""
Sanitize content of any form component.
The tooltip, description and label content will be replaced with a sanitized version.
All tags and attributes that aren't explicitly allowed, are removed from the
component content.
:arg component: the component data.
"""
if "tooltip" in component:
component["tooltip"] = sanitize_html_content(component["tooltip"])
if "description" in component:
component["description"] = sanitize_html_content(component["description"])
if "label" in component:
component["label"] = sanitize_html_content(component["label"])
Loading

0 comments on commit 4cb4b65

Please sign in to comment.