# Session Learnings: ephron.ren QA (2026-05-03) ## Environment Facts - 5 services: Home(8000), Auth(8001), Blog(8002), Canvas(8003), Prompt(8004) - Auth: FastAPI + Tortoise ORM, `.ephron.ren` domain cookie - RBAC: user(10) < admin(20) < owner(30) - CSRF: `{unix_timestamp}:{sha256_hex}` format, 75 chars, per-GET refresh - Rate limits: login 5/min, register 6/hour, comments 6/min, likes 11/min, save 21/min ## High-Value Findings (Reproducible Patterns) ### CSP script-src-elem Override (Critical) - **Symptom**: Buttons with `onclick="fnName()"` do nothing, `typeof fnName` returns `undefined` - **Root cause**: `script-src-elem 'self' https://cdn.example.com` overrides `script-src 'unsafe-inline'` - **Detection**: `curl -sI URL | grep content-security-policy`, look for `script-src-elem` without `'unsafe-inline'` - **Impact**: All inline JS blocked → save/publish/discard buttons broken, client-only validation bypassed ### CSP form-action Blocks Cross-Origin Redirects (Critical) - **Date**: 2026-05-05 - **Symptom**: Login form submits (POST appears in network tab), server sets cookie, but browser stays on login page — no redirect - **Root cause**: CSP `form-action 'self'` on the 303 redirect response blocks navigation to cross-origin targets - **Reproduction**: 1. Visit `https://auth.ephron.ren/login?redirect=aHR0cHM6Ly93d3cuZXBocm9uLnJlbi8=` (redirect=base64 of `https://www.ephron.ren/`) 2. Fill username/password, click submit 3. Browser sends POST to `/api/login` (same origin ✅ allowed) 4. Server returns 303 to `https://www.ephron.ren/` with CSP header containing `form-action 'self'` 5. Browser blocks redirect: `https://www.ephron.ren/` ≠ `self` (`https://auth.ephron.ren`) - **Controlled test**: Same-origin redirect (`auth.ephron.ren/admin`) works fine; cross-origin fails - **Console error**: `Sending form data to 'https://auth.ephron.ren/api/login' violates the following Content Security Policy directive: "form-action 'self'"` - **Fix**: Skip CSP header on 303 responses (empty body, no protection value), or use JS redirect - **Affected pages**: ALL pages that redirect to login with a cross-origin redirect target (www/blog/canvas/prompt subdomains) - **Key source files**: `shared/security_headers.py` (CSP middleware), `auth/src/routes/api.py` (login endpoint), `auth/src/utils/redirect.py` (redirect validation) ### Server-Side Password Validation Missing - **Test**: `curl -X POST /api/register -d 'username=test&password=123&password_confirm=456&invite_code=CODE'` - **Expected**: 400/422 with validation error - **Actual**: 303 redirect (registration succeeds with weak/mismatched passwords) - **Root cause**: Validation only in client JS (blocked by CSP) - **Lesson**: Always test form validation with curl, not just browser ### Fulltext Search Silent Failure - **Test**: `GET /posts?q=openclaw&mode=fulltext` returns 0 results, `mode=simple` returns 6 - **Root cause**: BM25 index not built or jieba tokenizer not installed - **Detection**: Compare simple vs fulltext results for same query ### API Auth Order Bug - **Test**: `POST /api/service/posts` without token, with invalid body - **Expected**: 401 (unauthenticated) - **Actual**: 422 (body validation error — leaks endpoint info) - **Root cause**: Pydantic validation middleware runs before auth middleware ## Delegate Task Sizing - Curl-only tasks: max ~15-20 test cases per delegate (30+ cases timeout at 600s) - Browser tasks: max ~5-8 interactions per delegate (each = 10-30s) - Use `execute_code` with `from hermes_tools import terminal` for fastest execution - Parallel delegates: 3 max, but each should be independently scoped ## Cookie Jar Synchronization - CSRF token changes on every GET request - Must use SAME cookie jar for GET (extract token) and POST (submit form) - Multiple CSRF tokens on one page (one per form) — extract from specific form context - Cross-service cookies: Domain=.ephron.ren should work for all subdomains - If cross-service test fails, check cookie jar file, not the cookie itself ## Content Restoration Pattern (Playwright) When homepage/admin content is accidentally overwritten, restore via Playwright: 1. Prepare JSON with original content (experience/projects/skills/contact/footer) 2. Login → navigate to admin page 3. Use `page.evaluate()` to set form fields by `id=` (NOT `name=` — admin forms use id): ```js document.getElementById('contact_email').value = '...'; document.getElementById('footer_copyright').value = '...'; ``` 4. Set structured data: `initialContent.experience = [...]; renderExperience();` 5. Set `is_draft: false` for items that should be published 6. Collect and publish: ```js const content = collectFormData(); document.querySelector('input[name="content_json"]').value = JSON.stringify(content); // Find form with content_json input, set action=/admin/publish, submit ``` 7. Verify with `curl -s https://site/` checking for restored content strings ## Form Field Discovery - Admin page fields may use `id=` instead of `name=` — check both: ```bash curl -s -b cookies /admin | grep -oP 'id="[^"]*"' | sort -u curl -s -b cookies /admin | grep -oP 'name="[^"]*"' | sort -u ``` - `collectFormData()` reads from visible form elements, not hidden `content_json` - Setting `content_json` directly is overwritten by `collectFormData()` on submit ## Playwright vs curl for Form Submission - **curl**: CSRF token sync is fragile (token changes per-GET, cookie jar must match) - **Playwright**: Handles cookies/CSRF automatically, but CSP may block inline JS - **Best approach**: Use Playwright + `page.evaluate()` to bypass CSP-blocked functions - **Pattern**: Set form fields via JS → call `collectFormData()` → set `content_json` → submit form directly