drop-validation-hardening-plan
Plan: Drop Validation Hardening
Research Summary
What the documentation says (spec vs reality)
Sources reviewed:
project/architecture/api-specification.md— API contract, field examples, error codesproject/architecture/architecture-document.md— User requirements from vilkår.html (legally binding)project/docs/security-qa-audit.md— Issue #14 (email), #19 (password), #9 (amounts)project/docs/drop-qa-rapport.md— QA findings C-1 through L-10src/lib/middleware/validation.ts— Existing validators (UNUSED by any route)src/app/api/auth/register/route.ts— Current registration validationsrc/app/onboarding/page.tsx— Current frontend validation
Gap Analysis
| Field | Spec / Audit Requirement | Current Implementation | Gap |
|---|---|---|---|
Regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/ (audit #14) |
email.includes("@") |
YES — accepts @, a@, @ @ |
|
| Password | 12+ chars, uppercase, lowercase, digit (audit #19) | length >= 8, no complexity |
YES — "12345678" passes |
| First/Last Name | validateName() exists in validation.ts — 1-100 chars, no script tags, sanitized |
typeof === "string" only |
YES — "123", XSS, 100K chars pass |
| Phone | +47 Norwegian format (architecture doc 1.4) |
No validation at all | YES — any string passes |
| Age | 18+ from vilkår.html (architecture doc 1.4) | Not implemented | YES — but deferred (needs BankID) |
| Amount (remittance) | 100-50,000 NOK, 2 decimal places, finite (audit #9) | 100-50,000 check, isFinite | PARTIAL — no decimal precision |
| Amount (QR) | 1-100,000 NOK, 2 decimals | 1-100,000 check, isFinite | PARTIAL — no decimal precision |
| Bank account | IBAN validation (validation.ts has validateIBAN) |
No validation | YES — but separate scope |
| Currency | NOK, RSD, BAM, PLN, PKR, TRY, EUR | Missing RSD, TRY, PKR, NOK | YES — but unused validator |
Existing assets (ready to wire up)
src/lib/middleware/validation.ts already has:
validateEmail()— proper regexvalidatePhone()— international+format, 8-15 digitsvalidateAmount()— positive, max 2 decimalsvalidateIBAN()— format + mod-97 checksumvalidatePIN()— exactly 4 digitsvalidateName()— 1-100 chars, no script tagssanitizeText()— strips HTML, control chars, max lengthvalidateCurrency()— needs RSD, TRY, PKR, NOK added
Key insight: The validators EXIST but are NEVER IMPORTED. The middleware/ directory is entirely dead code (QA rapport C-1). The fix is to wire existing validators into the actual routes.
Objective
Wire existing validation utilities into all API routes and frontend forms. Close the gap between documented requirements and actual implementation. Do NOT create new validators — use what exists in validation.ts, extend where needed.
Scope
In scope:
- Registration API — email, password, name, phone validation
- Registration frontend — matching client-side checks
- Login frontend — email format check
- Amount validation — add decimal precision check to remittance + QR payment routes
- Update existing tests to match new validation rules
- Currency validator — add missing currencies
Out of scope (separate tasks):
- Age verification (needs BankID integration)
- IBAN validation for recipients (separate feature)
- CSRF protection (separate security task)
- Audit logging (separate task)
- Password complexity upgrade to 12+ (BREAKING CHANGE — needs migration plan, keep 8 for now but add complexity)
Team Orchestration
Team Members
| ID | Name | Role | Agent Type |
|---|---|---|---|
| B1 | validation-builder | Implement validation wiring | builder |
| V1 | validation-validator | Verify all validation works | validator |
Step-by-Step Tasks
Phase 1: Backend Validation (API Routes)
Task 1: Wire validators into registration API
- Owner: B1
- BlockedBy: none
- Files:
src/app/api/auth/register/route.ts,src/lib/middleware/validation.ts - Changes:
- Import
validateEmail,validateName,sanitizeText,validatePhonefrom@/lib/middleware/validation - Replace
!email.includes("@")with!validateEmail(email) - Replace
!firstName || typeof firstName !== "string"with!validateName(firstName) - Replace
!lastName || typeof lastName !== "string"with!validateName(lastName) - Add phone validation:
if (phone && !validatePhone(phone))error - Sanitize names before storing:
sanitizeText(firstName, 100),sanitizeText(lastName, 100) - Add password complexity: min 8 chars + at least 1 letter + at least 1 digit (soft upgrade, not full 12-char yet)
- Add
validateCurrencyupdate: add missing currencies (RSD, TRY, PKR, NOK)
- Import
- Acceptance:
-
user@rejected (no domain) -
@domain.comrejected (no local part) -
user [email protected]rejected (spaces) -
[email protected]accepted -
123as firstName rejected (contains only digits? No — validateName allows digits, just checks length + no script tags. This is acceptable.) -
<script>alert(1)</script>as name rejected - Empty firstName rejected
- 200+ char firstName truncated to 100
- Phone without
+prefix rejected (if provided) - Phone
+4712345678accepted - Password
12345678rejected (no letter) - Password
abcdefghrejected (no digit) - Password
abc12345accepted (letter + digit + 8 chars)
-
Task 2: Wire validators into amount routes
- Owner: B1
- BlockedBy: none
- Files:
src/app/api/transactions/remittance/route.ts,src/app/api/transactions/qr-payment/route.ts - Changes:
- Import
validateAmountfrom@/lib/middleware/validation - Add decimal precision check before range check:
if (Math.round(amount * 100) !== amount * 100)→ 400 error - Existing range checks stay (100-50K for remittance, 1-100K for QR)
- Import
- Acceptance:
-
100.999rejected (3 decimals) -
100.99accepted (2 decimals) -
100accepted (0 decimals) - Existing range tests still pass
-
Task 3: Validate Task 1 + Task 2
- Owner: V1
- BlockedBy: 1, 2
- Acceptance:
- Read all modified files, verify imports are correct
- Verify no breaking changes to existing passing tests
- Run
npm test— all 120 Vitest tests pass - Run
npx playwright test— all 75 e2e tests pass (may need test updates)
Phase 2: Frontend Validation (Client-side)
Task 4: Add proper validation to registration form
- Owner: B1
- BlockedBy: 3 (backend must be validated first)
- Files:
src/app/onboarding/page.tsx - Changes:
- Replace
!email.includes("@")with proper regex check matching backend - Add inline error for email format: "Ugyldig e-postformat"
- Add password complexity check: show "Passord må inneholde bokstaver og tall"
- Add name format hint if script tags detected: "Ugyldig tegn i navn"
- Add phone format hint: "Norsk telefonnummer påkrevd (+47...)"
- Keep button disabled logic, but add per-field inline errors
- Replace
- Acceptance:
-
user@shows email format error immediately on blur -
12345678password shows complexity error - Valid form submits normally
- Error messages in Norwegian
-
Task 5: Add email validation to login form
- Owner: B1
- BlockedBy: 3
- Files:
src/app/login/page.tsx - Changes:
- Add email format check matching backend regex
- Show "Ugyldig e-postformat" if email doesn't match on submit
- Acceptance:
- Invalid email format shows Norwegian error
- Valid email + wrong password shows credential error (not format error)
Task 6: Validate Task 4 + Task 5
- Owner: V1
- BlockedBy: 4, 5
- Acceptance:
- Read all modified frontend files
- Verify error messages are in Norwegian
- Run
npx playwright test— all 75 e2e tests pass - Verify no regressions in user-flows or input-chaos tests
Phase 3: Test Updates
Task 7: Update e2e tests for new validation rules
- Owner: B1
- BlockedBy: 6
- Files:
tests/e2e/input-chaos.spec.ts,tests/e2e/user-flows.spec.ts - Changes:
- Update password boundary tests — "12345678" now fails (no letter), use "pass1234" instead
- Update any test assertions that depend on old weak validation
- Add new positive test: valid registration with proper fields
- Verify all 75 tests pass with the new validation
- Acceptance:
-
npx playwright test— 75/75 pass -
npm test— 120/120 pass - No test uses weak input that now gets rejected without updating the assertion
-
Task 8: Final validation — full test suite
- Owner: V1
- BlockedBy: 7
- Acceptance:
-
npm test— 120/120 pass (5 consecutive runs) -
npx playwright test— 75/75 pass - Manual check: registration form rejects
<script>in name - Manual check: registration form rejects
user@email - Manual check: registration accepts valid Norwegian data
-
Files Modified
Backend (API)
src/app/api/auth/register/route.ts— wire validatorssrc/app/api/transactions/remittance/route.ts— decimal precisionsrc/app/api/transactions/qr-payment/route.ts— decimal precisionsrc/lib/middleware/validation.ts— add missing currencies
Frontend
src/app/onboarding/page.tsx— proper client-side validationsrc/app/login/page.tsx— email format check
Tests
tests/e2e/input-chaos.spec.ts— update for new rulestests/e2e/user-flows.spec.ts— update if needed
Validation Commands
# Unit + integration tests
npm test
# E2E tests (both suites)
npx playwright test
# Quick API validation
curl -s http://localhost:3000/api/auth/register \
-X POST -H "Content-Type: application/json" \
-d '{"email":"user@","password":"12345678","firstName":"<script>","lastName":"Test"}' | jq .
# Expected: 422 with validation errors
Risk Assessment
- Low risk: Wiring existing validators — they're already written and tested in isolation
- Medium risk: Frontend changes may break e2e test assertions that depend on old behavior
- Mitigation: Phase 3 explicitly updates tests after backend + frontend are done
- NOT breaking existing users: Password min stays at 8 (just adds letter+digit requirement). Demo user "demo1234" has both letters and digits — still works.
No comments to display
No comments to display