Restrict user input to more sensible ranges #251

Merged
tobru merged 2 commits from 223-input-validation into main 2025-10-27 10:31:21 +00:00
2 changed files with 138 additions and 7 deletions

View file

@ -6,6 +6,7 @@ from auditlog.registry import auditlog
from django.conf import settings from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import RegexValidator
from django.db import models, transaction from django.db import models, transaction
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -20,7 +21,18 @@ from servala.core.odoo import CLIENT
class Organization(ServalaModelMixin, models.Model): 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. # 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 # 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. # not be null in practical use.

View file

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -8,7 +9,17 @@ from servala.core.odoo import get_invoice_addresses, get_odoo_countries
from servala.frontend.forms.mixins import HtmxMixin from servala.frontend.forms.mixins import HtmxMixin
ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+"
class OrganizationForm(HtmxMixin, ModelForm): 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): # def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs) # super().__init__(*args, **kwargs)
# if self.instance and self.instance.has_inherited_billing_entity: # if self.instance and self.instance.has_inherited_billing_entity:
@ -16,9 +27,48 @@ class OrganizationForm(HtmxMixin, ModelForm):
class Meta: class Meta:
model = Organization model = Organization
fields = ("name",) fields = ("name",)
widgets = {
"name": forms.TextInput(
attrs={
"maxlength": "100",
"pattern": ORG_NAME_PATTERN,
"title": _(
"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): 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( billing_processing_choice = forms.ChoiceField(
choices=[ choices=[
("existing", _("Use an existing billing address")), ("existing", _("Use an existing billing address")),
@ -34,17 +84,86 @@ class OrganizationCreateForm(OrganizationForm):
) )
# Fields for creating a new billing address in Odoo, prefixed with 'invoice_' # 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_street = forms.CharField(
invoice_street2 = forms.CharField(label=_("Line 2"), required=False, max_length=100) label=_("Line 1"),
invoice_city = forms.CharField(label=_("City"), required=False, max_length=100) required=False,
invoice_zip = forms.CharField(label=_("Postal Code"), required=False, max_length=20) 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( invoice_country = forms.ChoiceField(
label=_("Country"), label=_("Country"),
required=False, required=False,
choices=get_odoo_countries(), choices=get_odoo_countries(),
) )
invoice_email = forms.EmailField(label=_("Billing Email"), required=False) invoice_email = forms.EmailField(
invoice_phone = forms.CharField(label=_("Phone"), required=False, max_length=30) 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): class Meta(OrganizationForm.Meta):
pass pass