98 lines
5.7 KiB
Markdown
98 lines
5.7 KiB
Markdown
# 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
|