Skip to content

Use the timing-safe compare() helper for the CSRF formkey check - #2650

Merged
leonelcamara merged 1 commit into
web2py:masterfrom
Nexory:harden/formkey-constant-time
Jul 10, 2026
Merged

Use the timing-safe compare() helper for the CSRF formkey check#2650
leonelcamara merged 1 commit into
web2py:masterfrom
Nexory:harden/formkey-constant-time

Conversation

@Nexory

@Nexory Nexory commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

FORM.accepts() validates the anti-CSRF form key with a plain list-membership test:

if not (formkey and formkeys and formkey in formkeys):

formkey in formkeys compares the attacker-supplied _formkey against each stored key using CPython's ==, which short-circuits on the first differing character and is not constant-time.

web2py already ships a timing-safe comparison helper for exactly this, gluon.utils.compare (documented "Compares two strings and not vulnerable to timing attacks", backed by hmac.compare_digest), and already uses it for the analogous signature check in the same file:

# gluon/html.py:581
return compare(original_sig, sig)

This applies that same helper to the formkey check, so the anti-CSRF token comparison is timing-safe and consistent with the rest of the module:

if not (formkey and formkeys and any(compare(formkey, k) for k in formkeys)):

compare is already imported at the top of the file, so no new import is needed. Behavior is unchanged: a valid formkey is still accepted and an invalid one is still rejected; only the per-key comparison is now constant-time.

Testing

gluon/tests/test_html.py and gluon/tests/test_form.py pass unchanged (86 passed). I also verified directly that a valid formkey is accepted and a wrong formkey is rejected after the change.

This is the same hardening class as #1321 ("fixed timing attack in gluon.utils.compare"), applied to the formkey comparison site.

FORM.accepts() compared the anti-CSRF _formkey with `formkey in formkeys`,
which uses CPython's short-circuiting `==` and is not constant-time.
gluon.utils.compare (hmac.compare_digest-backed, already used for the
signature check at gluon/html.py:581) is applied instead, so the token
comparison is timing-safe and consistent with the rest of the module.
compare is already imported; behavior is unchanged.

Same hardening class as web2py#1321.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 50.71%. Comparing base (1462a3e) to head (fac9c60).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2650      +/-   ##
==========================================
+ Coverage   50.70%   50.71%   +0.01%     
==========================================
  Files          42       42              
  Lines       15500    15500              
  Branches     3409     3409              
==========================================
+ Hits         7859     7861       +2     
+ Misses       6599     6598       -1     
+ Partials     1042     1041       -1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@leonelcamara
leonelcamara merged commit b9da23f into web2py:master Jul 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants