From eea743e5ae9f25a2d9dd7b5189f42f9e9f9942d5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 24 Oct 2025 12:40:10 +0200 Subject: [PATCH 1/2] Restrict user input to more sensible ranges ref #223 --- src/servala/core/models/organization.py | 14 ++- src/servala/frontend/forms/organization.py | 116 +++++++++++++++++++-- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 646849f..09205dc 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -6,6 +6,7 @@ from auditlog.registry import auditlog from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from django.core.mail import send_mail +from django.core.validators import RegexValidator from django.db import models, transaction from django.http import HttpRequest from django.utils.functional import cached_property @@ -20,7 +21,18 @@ from servala.core.odoo import CLIENT class Organization(ServalaModelMixin, models.Model): - name = models.CharField(max_length=100, verbose_name=_("Name")) + name = models.CharField( + max_length=32, + verbose_name=_("Name"), + validators=[ + RegexValidator( + regex=r"^[A-Za-z0-9\s]+$", + message=_( + "Organization name can only contain letters, numbers, and spaces." + ), + ) + ], + ) # The namespace is generated as "org-{id}" in accordance with RFC 1035 Label Names. # It is nullable as we need to write to the database in order to read the ID, but should # not be null in practical use. diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 27e6a09..96b06e0 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -1,5 +1,6 @@ from django import forms from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ @@ -16,9 +17,43 @@ class OrganizationForm(HtmxMixin, ModelForm): class Meta: model = Organization fields = ("name",) + widgets = { + "name": forms.TextInput( + attrs={ + "maxlength": "32", + "pattern": "[A-Za-z0-9\\s]+", + "title": _( + "Organization name can only contain letters, numbers, and spaces" + ), + } + ), + } class OrganizationCreateForm(OrganizationForm): + address_validator = RegexValidator( + regex=r"^[\w\s\.,\-/()\']+$", + message=_( + "This field can only contain letters, numbers, spaces, and basic punctuation (.,-/()')." + ), + ) + city_validator = RegexValidator( + regex=r"^[\w\s\-\']+$", + message=_("City name contains invalid characters."), + ) + postal_code_validator = RegexValidator( + regex=r"^[\w\s\-]+$", + message=_( + "Postal code can only contain letters, numbers, spaces, and hyphens." + ), + ) + phone_validator = RegexValidator( + regex=r"^[0-9\s\+\-()]+$", + message=_( + "Phone number can only contain numbers, spaces, and basic punctuation (+,-,())." + ), + ) + billing_processing_choice = forms.ChoiceField( choices=[ ("existing", _("Use an existing billing address")), @@ -34,17 +69,86 @@ class OrganizationCreateForm(OrganizationForm): ) # Fields for creating a new billing address in Odoo, prefixed with 'invoice_' - invoice_street = forms.CharField(label=_("Line 1"), required=False, max_length=100) - invoice_street2 = forms.CharField(label=_("Line 2"), required=False, max_length=100) - invoice_city = forms.CharField(label=_("City"), required=False, max_length=100) - invoice_zip = forms.CharField(label=_("Postal Code"), required=False, max_length=20) + invoice_street = forms.CharField( + label=_("Line 1"), + required=False, + max_length=128, + validators=[address_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "128", + "title": _( + "Letters, numbers, spaces, and basic punctuation allowed. Emoji not allowed." + ), + } + ), + ) + invoice_street2 = forms.CharField( + label=_("Line 2"), + required=False, + max_length=128, + validators=[address_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "128", + "title": _( + "Letters, numbers, spaces, and basic punctuation allowed. Emoji not allowed." + ), + } + ), + ) + invoice_city = forms.CharField( + label=_("City"), + required=False, + max_length=64, + validators=[city_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "64", + "title": _( + "Letters, spaces, hyphens, and apostrophes allowed. Emoji not allowed." + ), + } + ), + ) + invoice_zip = forms.CharField( + label=_("Postal Code"), + required=False, + max_length=20, + validators=[postal_code_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "20", + "title": _( + "Letters, numbers, spaces, and hyphens allowed. Emoji not allowed." + ), + } + ), + ) invoice_country = forms.ChoiceField( label=_("Country"), required=False, choices=get_odoo_countries(), ) - invoice_email = forms.EmailField(label=_("Billing Email"), required=False) - invoice_phone = forms.CharField(label=_("Phone"), required=False, max_length=30) + invoice_email = forms.EmailField( + label=_("Billing Email"), + required=False, + max_length=254, + widget=forms.EmailInput(attrs={"maxlength": "254"}), + ) + invoice_phone = forms.CharField( + label=_("Phone"), + required=False, + max_length=30, + validators=[phone_validator], + widget=forms.TextInput( + attrs={ + "maxlength": "30", + "pattern": r"[0-9\s\+\-()]+", + "title": _("Only numbers, spaces, and basic punctuation allowed"), + } + ), + ) class Meta(OrganizationForm.Meta): pass -- 2.49.1 From d3e38a0ecb00f17e7b2cd20bdc2e4198cc2b0e4f Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 27 Oct 2025 11:30:59 +0100 Subject: [PATCH 2/2] relaxed organization name validation pattern --- src/servala/frontend/forms/organization.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py index 96b06e0..86ba0ab 100644 --- a/src/servala/frontend/forms/organization.py +++ b/src/servala/frontend/forms/organization.py @@ -9,7 +9,17 @@ from servala.core.odoo import get_invoice_addresses, get_odoo_countries from servala.frontend.forms.mixins import HtmxMixin +ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+" + + class OrganizationForm(HtmxMixin, ModelForm): + name_validator = RegexValidator( + regex=f"^{ORG_NAME_PATTERN}$", + message=_( + "Organization name can only contain letters, numbers, spaces, and common punctuation (-.,&'()+)." + ), + ) + # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # if self.instance and self.instance.has_inherited_billing_entity: @@ -20,15 +30,20 @@ class OrganizationForm(HtmxMixin, ModelForm): widgets = { "name": forms.TextInput( attrs={ - "maxlength": "32", - "pattern": "[A-Za-z0-9\\s]+", + "maxlength": "100", + "pattern": ORG_NAME_PATTERN, "title": _( - "Organization name can only contain letters, numbers, and spaces" + "Organization name can contain letters, numbers, spaces, and common punctuation (-.,&'()+). Emoji not allowed." ), } ), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["name"].validators.append(self.name_validator) + self.fields["name"].max_length = 100 + class OrganizationCreateForm(OrganizationForm): address_validator = RegexValidator( -- 2.49.1