5.7 KiB
5.7 KiB
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.rendomain 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 fnNamereturnsundefined - Root cause:
script-src-elem 'self' https://cdn.example.comoverridesscript-src 'unsafe-inline' - Detection:
curl -sI URL | grep content-security-policy, look forscript-src-elemwithout'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:
- Visit
https://auth.ephron.ren/login?redirect=aHR0cHM6Ly93d3cuZXBocm9uLnJlbi8=(redirect=base64 ofhttps://www.ephron.ren/) - Fill username/password, click submit
- Browser sends POST to
/api/login(same origin ✅ allowed) - Server returns 303 to
https://www.ephron.ren/with CSP header containingform-action 'self' - Browser blocks redirect:
https://www.ephron.ren/≠self(https://auth.ephron.ren)
- Visit
- 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=fulltextreturns 0 results,mode=simplereturns 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/postswithout 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_codewithfrom hermes_tools import terminalfor 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:
- Prepare JSON with original content (experience/projects/skills/contact/footer)
- Login → navigate to admin page
- Use
page.evaluate()to set form fields byid=(NOTname=— admin forms use id):document.getElementById('contact_email').value = '...'; document.getElementById('footer_copyright').value = '...'; - Set structured data:
initialContent.experience = [...]; renderExperience(); - Set
is_draft: falsefor items that should be published - Collect and publish:
const content = collectFormData(); document.querySelector('input[name="content_json"]').value = JSON.stringify(content); // Find form with content_json input, set action=/admin/publish, submit - Verify with
curl -s https://site/checking for restored content strings
Form Field Discovery
- Admin page fields may use
id=instead ofname=— check both: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 hiddencontent_json- Setting
content_jsondirectly is overwritten bycollectFormData()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()→ setcontent_json→ submit form directly