# Code Review — Security | Field | Value | |---|---| | Module | `src/Server/ZB.MOM.WW.OtOpcUa.Security` | | Reviewer | Claude Code | | Review date | 2026-06-19 | | Commit reviewed | `7286d320` | | Status | Reviewed | | Open findings | 0 | ## Checklist coverage A comprehensive review completes every category, recording "No issues found" where a category produced nothing rather than leaving it blank. | # | Category | Result | |---|---|---| | 1 | Correctness & logic bugs | Security-001 (open redirect on successful login) | | 2 | OtOpcUa conventions | No issues found | | 3 | Concurrency & thread safety | No issues found | | 4 | Error handling & resilience | No issues found | | 5 | Security | Security-001 (open redirect), Security-002 (stale doc) | | 6 | Performance & resource management | No issues found | | 7 | Design-document adherence | No issues found | | 8 | Code organization & conventions | No issues found | | 9 | Testing coverage | Security-001 was untested (regression test added) | | 10 | Documentation & comments | Security-002 (stale LdapOptions comment) | ## Findings ### Security-001 | Field | Value | |---|---| | Severity | High | | Category | Security | | Location | `src/Server/ZB.MOM.WW.OtOpcUa.Security/Endpoints/AuthEndpoints.cs:135` | | Status | Resolved | **Description:** The `/auth/login` endpoint (form POST path) redirects to `returnUrl` on successful authentication without validating that the URL is local to the application. An attacker can craft a phishing link such as `https://admin.example.com/login?returnUrl=https%3A%2F%2Fevil.com` — the shared `LoginCard` component echoes the value verbatim into the hidden `returnUrl` form field (the `LoginCard.razor` security comment explicitly states: "the consuming app's POST handler MUST validate it is a local/relative URL before redirecting to prevent open-redirect"). After a successful bind the endpoint issues `Results.Redirect("https://evil.com")`, silently forwarding the authenticated user to an attacker-controlled site. The `RedirectToLogin.razor` Blazor component's own `returnUrl` generation is safe (`Nav.ToBaseRelativePath` always produces a relative path), but nothing prevents an attacker from directly constructing the URL. The normal cookie-auth challenge path also produces only relative paths. The gap is the absence of server-side local-URL validation on the consumed form field before redirecting. **Recommendation:** Replace `Results.Redirect(returnUrl)` with a local-URL guard: use `Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)` to validate the URL is relative before redirecting; fall back to `"/"` for absolute or malformed values. `Results.LocalRedirect` could alternatively be used but throws on invalid input (which the endpoint would need to catch). **Resolution:** Fixed 2026-06-19. Added `IsLocalUrl` helper that rejects absolute and protocol-relative URLs; success-path now falls back to `"/"` on invalid input. Regression test `Login_with_absolute_returnUrl_does_not_open_redirect` added to `AuthEndpointsIntegrationTests`. --- ### Security-002 | Field | Value | |---|---| | Severity | Low | | Category | Documentation & comments | | Location | `src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapOptions.cs:9` | | Status | Resolved | **Description:** The XML doc comment on `LdapOptions` references `C:\publish\glauth\auth.md` as the source for dev LDAP defaults. Per `CLAUDE.md` (§ LDAP Authentication) the per-VM NSSM GLAuth at `C:\publish\glauth\` is obsolete — the shared GLAuth now runs on the Linux Docker host at `10.100.0.35:3893`. A developer following the stale reference would look for a file that no longer exists on the machine. **Recommendation:** Update the doc comment to remove the stale local-path reference and direct readers to `docs/security.md` and `CLAUDE.md` §"LDAP Authentication" for dev LDAP setup. **Resolution:** Fixed 2026-06-19. Stale `C:\publish\glauth\auth.md` reference replaced with pointer to `docs/security.md`.