Skip to content

Commit

Permalink
Remove form_nonce validator and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Apr 4, 2024
1 parent a8dc812 commit 77e14ce
Show file tree
Hide file tree
Showing 4 changed files with 4 additions and 73 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exclude = src/baseframe/static
enable-extensions = G
accept-encodings = utf-8
classmethod-decorators=classmethod, declared_attr
pytest-fixture-no-parentheses = false

[pycodestyle]
max-line-length = 88
Expand Down
33 changes: 2 additions & 31 deletions src/baseframe/forms/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

import typing as t
import typing_extensions as te
import uuid
import warnings
from threading import Lock

import wtforms
from flask import current_app
Expand All @@ -15,7 +13,7 @@
from wtforms import Field as WTField
from wtforms.utils import unset_value

from ..extensions import __, asset_cache
from ..extensions import __
from ..signals import form_validation_error, form_validation_success
from . import (
fields as bfields,
Expand Down Expand Up @@ -98,40 +96,13 @@
}


_nonce_lock = Lock()


def _nonce_cache_key(nonce: str) -> str:
return 'form_nonce/' + nonce


def _nonce_validator(form: Form, field: bfields.Field) -> None:
# Check for already-used form nonce
if field.data:
with _nonce_lock:
# nonce_lock prevents parallel requests from attempting to use the same
# nonce in a multi-threaded deployment. As a thread lock, it is ineffective
# in a multi-process deployment, as is typical when using uwsgi or gunicorn.
nonce_cache_key = _nonce_cache_key(field.data)
nonce_cache_hit = asset_cache.get(nonce_cache_key)
if nonce_cache_hit is not None:
raise bvalidators.StopValidation(form.form_nonce_error)
# Mark this nonce as used for 10 seconds
asset_cache.set(_nonce_cache_key(field.data), True, 10)
# Set a new nonce. This is a conscious deviation from the convention for
# validators, which are expected to validate but not modify the data, leaving
# that to filters. However, filters run before validators, and the form nonce
# is nonsense data that will be imminently discarded, so this is okay here.
field.data = field.default()


class Form(BaseForm):
"""Form with additional methods."""

__expects__: t.Iterable[str] = ()
__returns__: t.Iterable[str] = ()

form_nonce = bfields.NonceField("Nonce", default=lambda: uuid.uuid4().hex)
form_nonce = bfields.NonceField("Nonce", default='')
form_nonce_error = __("This form has already been submitted")

def __init_subclass__(cls, **kwargs: t.Any) -> None:
Expand Down
39 changes: 0 additions & 39 deletions tests/baseframe_tests/forms/validators_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,45 +231,6 @@ def test_html_snippet_invalid_urls(app, tforms) -> None:
assert not tforms.all_urls_form.validate()


@pytest.mark.usefixtures('ctx')
def test_nonce_form_on_success(tforms) -> None:
"""A form with a nonce cannot be submitted twice."""
formdata = MultiDict({field.name: field.data for field in tforms.nonce_form})
nonce = tforms.nonce_form.form_nonce.data
assert nonce
assert tforms.nonce_form.validate() is True
# Nonce changes on each submit
assert nonce != tforms.nonce_form.form_nonce.data
assert not tforms.nonce_form.form_nonce.errors
# Now restore old form contents
tforms.nonce_form.process(formdata=formdata)
# Second attempt on the same form contents will fail
assert tforms.nonce_form.validate() is False
assert tforms.nonce_form.form_nonce.errors


@pytest.mark.usefixtures('ctx')
def test_nonce_form_on_failure(tforms) -> None:
"""Form resubmission is not blocked (via the nonce) when validation fails."""
tforms.emoji_form.process(
formdata=MultiDict(
{'emoji': 'not-emoji', 'form_nonce': tforms.emoji_form.form_nonce.data}
)
)
assert tforms.emoji_form.validate() is False
assert not tforms.emoji_form.form_nonce.errors
formdata = MultiDict(
{'emoji': '👍', 'form_nonce': tforms.emoji_form.form_nonce.data}
)
tforms.emoji_form.process(formdata=formdata)
assert tforms.emoji_form.validate() is True
assert not tforms.emoji_form.form_nonce.errors
# Second attempt on the same form data will fail
tforms.emoji_form.process(formdata)
assert tforms.emoji_form.validate() is False
assert tforms.emoji_form.errors


@pytest.mark.usefixtures('ctx')
def test_no_schemes() -> None:
class UrlForm(forms.Form):
Expand Down
4 changes: 1 addition & 3 deletions tests/baseframe_tests/statsd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ class SimpleForm(forms.Form):
"Required", validators=[forms.validators.DataRequired()]
)

f = SimpleForm(meta={'csrf': False})
del f.form_nonce
return f
return SimpleForm(meta={'csrf': False})


def test_default_config(app, statsd) -> None:
Expand Down

0 comments on commit 77e14ce

Please sign in to comment.