Discovered during ad-hoc testing on PR #284. Full context in .claude/notes/2026-05-20-signup-flow-adhoc-findings.md — Finding 7 (non-username parts; the duplicate-username portion is fixed in commit 2fdbcd9 on PR #284).
signup_submit performs no validation on identity fields beyond CSRF. Every "Required" badge on the HTML form is required HTML5-attribute only.
| Tested |
Result |
No first_name → row created with first_name: null |
✓ accepted |
No last_name → row created with last_name: null |
✓ accepted |
| Empty everything except email + csrf → row created with all-null identity |
✓ accepted |
| Birthday in 2020 (5 years old) |
✓ accepted, no age check |
| Birthday in 2030 (future) |
✓ accepted |
birthday=not-a-date |
✓ accepted, stored verbatim |
5000-char first_name |
✓ accepted, no length cap |
PS1 policy is members must be 18+. The admin portal validates this on identity edits via validate_age_18_or_older in FIELD_VALIDATORS, but signup does not.
Suggested approach
Mirror the admin portal's FIELD_VALIDATORS + validate_update_data pattern. Add a validate_signup_data() function in code/DHMemberPortal/app.py called near the top of signup_submit. Covers:
- Required:
first_name, last_name, email, birthday, username, phone
- Length caps matching the HTML
maxlength: 100 / 100 / (email) / (date) / 16 / 20
- Birthday: ISO
YYYY-MM-DD, strptime-parseable, member must be 18+ at signup time, not in the future, not before 1900
- Phone: matches the existing client-side regex
- Username: matches
[a-zA-Z0-9_-]+, 3-16 chars
Incidentally closes Finding 6 (/signup/check-email bypass): once submit-time validation rejects malformed/missing fields, skipping the check-email step buys an attacker nothing.
Context: #283, PR #284
Discovered during ad-hoc testing on PR #284. Full context in
.claude/notes/2026-05-20-signup-flow-adhoc-findings.md— Finding 7 (non-username parts; the duplicate-username portion is fixed in commit2fdbcd9on PR #284).signup_submitperforms no validation on identity fields beyond CSRF. Every "Required" badge on the HTML form isrequiredHTML5-attribute only.first_name→ row created withfirst_name: nulllast_name→ row created withlast_name: nullbirthday=not-a-datefirst_namePS1 policy is members must be 18+. The admin portal validates this on identity edits via
validate_age_18_or_olderinFIELD_VALIDATORS, but signup does not.Suggested approach
Mirror the admin portal's
FIELD_VALIDATORS+validate_update_datapattern. Add avalidate_signup_data()function incode/DHMemberPortal/app.pycalled near the top ofsignup_submit. Covers:first_name,last_name,email,birthday,username,phonemaxlength: 100 / 100 / (email) / (date) / 16 / 20YYYY-MM-DD, strptime-parseable, member must be 18+ at signup time, not in the future, not before 1900[a-zA-Z0-9_-]+, 3-16 charsIncidentally closes Finding 6 (
/signup/check-emailbypass): once submit-time validation rejects malformed/missing fields, skipping the check-email step buys an attacker nothing.Context: #283, PR #284