# 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`.
