010aceb1ac
bindDn and bindPassword are now optional on LdapConfig (nullable
migration) and throughout the DTOs/service/client -- an admin can
leave both blank to connect to directories that permit anonymous
read access. LdapService.bind() falls back to an RFC 4513 anonymous
bind (empty DN + empty password) whenever either field is missing,
shared across testConnection, listGroups, and syncUsersForTenant.
Frontend: removed the required attribute from Bind-DN/Bind-Passwort,
added a placeholder hint ("leer = anonymous bind"), and the
"Verbindung testen" button now only needs a Server-URL to enable
(not bindDn+bindPassword). Config responses now return bindPassword
as null (not a misleading "********") when no password is set.
Verified locally: submitted only a Server-URL with both bind fields
empty and confirmed the request reached the anonymous-bind code path
(DNS failure for the unreachable test host, not a validation error).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
14 KiB
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260707-lgh | 01 | execute | 1 |
|
true |
|
|
Purpose: Restore visible favicons for CORP-protected sites without weakening SSRF posture or altering the creation-time icon-discovery flow.
Output: A new GET /favorites/:id/icon endpoint that streams the stored icon bytes for a row owned by the authenticated user (SSRF-guarded, size/timeout/content-type capped), and a frontend change pointing <img src> at that same-origin route via the existing /api-proxy rewrite.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/STATE.md @./CLAUDE.md @apps/api/src/favorites/favorites.controller.ts @apps/api/src/favorites/favorites.service.ts @apps/api/src/favorites/icon-discovery.service.ts @apps/web/src/components/dashboard/widgets/favorites-widget.tsx @apps/web/next.config.tsInterface facts (already read — do not re-derive):
- Controller scopes requests via
extractContext(req)returning{ userId, tenantId }; ownership on other routes (update/remove) is enforced in the service by matchinglink.userId !== userIdand throwingNotFoundException. Match this exact pattern. FavoriteLinkhasuserId,tenantId,iconUrl String?. The service's existing ownership check comparesuserIdonly — follow that (do NOT invent a tenantId-based check).icon-discovery.service.tscurrently has module-privateisPublicHttpUrl,isPrivateIpAddress,isBlockedHostname, and the manual-redirect loopfetchHtml(redirect: 'manual', per-hop re-validation, 4000ms AbortController timeout,MAX_HTML_CHARScap). These are the primitives to share.- Frontend:
<img src={fav.iconUrl}>is at favorites-widget.tsx around lines 361-373, guarded by{fav.iconUrl && (...)}with an existingonErrorhandler that hides the img. Client API calls useAPI_URL(favorites-api.ts) which resolves to the API origin; the browser reaches the API through the/api-proxy/:path*rewrite in next.config.ts.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
browser -> API (GET /favorites/:id/icon) |
Authenticated caller supplies a FavoriteLink id (not a URL) |
| API -> external favicon host | Server-side outbound fetch of a stored, previously-discovered URL |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-QFIP-01 | Information Disclosure (SSRF) | GET :id/icon fetch target | high | mitigate | Endpoint takes a FavoriteLink id, never a client-supplied URL. The URL fetched is the row's stored iconUrl loaded server-side — no arbitrary-URL proxy surface. All fetches go through the shared isPublicHttpUrl guard (private IP ranges, blocked hostnames, DNS resolution). |
| T-QFIP-02 | Information Disclosure (SSRF redirect) | fetchWithRedirectGuard | high | mitigate | Redirects handled redirect: 'manual' with per-hop re-validation via isPublicHttpUrl and a bounded MAX_REDIRECTS — reused from the existing HTML path, not re-implemented. |
| T-QFIP-03 | Information Disclosure (cross-user) | getIconBytes ownership check | high | mitigate | Row loaded then rejected with NotFoundException unless link.userId === userId; 404 (not 403) avoids leaking row existence — mirrors existing update/remove. |
| T-QFIP-04 | Denial of Service (resource exhaustion) | fetchIconBytes | medium | mitigate | ICON_FETCH_TIMEOUT_MS AbortController timeout + MAX_ICON_BYTES 1MB body cap prevent slow-loris and large-body memory exhaustion, mirroring existing HTML timeout/MAX_HTML_CHARS caps. |
| T-QFIP-05 | Tampering / Spoofing (content smuggling) | fetchIconBytes content-type gate | medium | mitigate | Response Content-Type must start with image/; non-image responses are rejected (502) rather than streamed to the browser. |
| T-QFIP-06 | Denial of Service (error masking) | controller error mapping | low | mitigate | Failures return real 404/502 statuses (never 200 + garbage); the frontend onError handler then cleanly reveals the letter fallback. |
| </threat_model> |
<success_criteria>
- CORP-protected favicons (claude.ai) render because the
<img>is same-origin. - No arbitrary-URL SSRF proxy exists — only id-based, ownership-scoped lookups.
- SSRF validation is shared, not duplicated;
discoverFavoriteIconUrlis unchanged. - Failures return 404/502; success carries a Cache-Control header. </success_criteria>