Commit bd9751a
authored
fix(auth): four admin actions were reachable by any authenticated user (#502)
* fix(auth): four admin actions were reachable by any authenticated user
Hand-read all thirteen no-admin-idor findings rather than treating the
count as a work queue. One is a real IDOR, four are semantic-auth
mismatches, two are genuinely guarded downstream, five take no object
reference at all, and one is scoped through its receiver.
The four semantic-auth ones are the serious half. SettingsController has a
consistent convention — every mutator is admin-required and only readers
carry the no-admin annotation — and these were the exceptions:
- testEmailConnection: caller-supplied smtpHost/smtpPort reach a DSN and
the server opens an outbound TCP connection to whatever was named. For
a non-admin that is an SSRF and internal port-scan primitive.
- updateEmailTemplate: the only write in the whole class carrying the
annotation. It writes app configuration, and the stored HTML is
rendered into real outbound mail, so any authenticated user could
rewrite the templates every recipient receives — and mint unbounded
email_template_* config rows besides.
- syncOrganisations: triggers a register-wide write sync with a
caller-chosen batch size.
- exportArchiMate: exports the WHOLE register while its sibling
exportOrgArchiMate exports one organisation — and the sibling has
carried verifyOrgExportPermission all along. The broader endpoint was
the unguarded one. The annotation is kept here deliberately: that
helper grants organisation-admins too, which is the tier the admin UI
relies on and which removing the annotation would drop.
The three email-template reads move with their write. No frontend code
calls any of those routes — the settings UI reads templates from the bulk
settings payload — verified with a positive control on a route that IS
called, so nothing breaks.
getSbomImportStatus is the real IDOR: a caller-supplied moduleVersieUuid
reached SbomImportService::getStatus(), whose lookup runs with rbac and
multitenancy both off. Guarded with authorizeManage()'s read tier — the
same two building blocks minus the editor-group requirement, since reading
a status is not managing an import. Checked rather than assumed: the module
schema carries a real read ACL, so this is a genuine scope and not the
default-open case an authorization-less schema would give. Refuses 404, not
403, so it cannot become an existence oracle.
Everything else carries a reason-bearing exemption naming the code path
that makes it safe, not a state of the world.
One finding is deliberately LEFT OPEN. getGebruikenForDeelnemer forces its
organisation filter after getParams(), so a caller cannot forge it — but it
passes an ARRAY where the app's canonical sibling passes a SCALAR, and the
query runs with rbac and multitenancy off. Whether OpenRegister honours
array-containment matching on a related-object array property is unverified,
and if it silently ignores the array form that scope is vacuous. It could
not be settled here: the available instance has zero gebruik rows, so a live
A/B would have returned empty under both forms and proved nothing. It gets
the fail-closed guard its canonical sibling already has, and keeps the
finding. Exempting it would have manufactured the coverage.
Nothing here makes anything return 200 that previously errored: every change
is deny-only. exportArchiMate's dead $organization filter parameter is a
real bug found on the way and is NOT repaired here — that is a feature
repair sitting behind a missing guard, and it belongs after the guard lands.
Refs #492.
* fix(sbom): guard inside the try, not ahead of it
My own regression, caught before it landed. authorizeRead() reaches
SbomImportService::resolveParentModuleUuid(), and OpenRegister's real
ObjectService::find() RE-THROWS DoesNotExistException for a well-formed but
non-existent uuid rather than returning null — importSbom()'s docblock
already records this and wraps its whole body for exactly that reason.
Placing the guard ahead of the try would have converted this endpoint's
clean 404 into a 500 for precisely the callers the guard was added for: a
non-admin passing an unknown id. The guard now sits inside the try, so the
not-found dialect is the same whichever call raises it.
* test(auth): pin the auth posture of the six hardened endpoints
The security-change-has-tests gate failed on the previous push, correctly:
that change moved auth posture in lib/ and touched no test. This is the
test, and it pins the near-miss rather than the diff.
It parses the controller source with Nextcloud's OWN annotation regex,
copied byte for byte from ControllerMethodReflector::reflect(). That is the
point. While writing the fix, three of these methods documented their own
hardening with the sentence "the endpoint must not declare @NoAdminRequired"
— and that token, at the start of a comment line, MATCHES that regex.
Nextcloud would have gone on treating the endpoint as non-admin-required:
the sentence explaining the removal would have undone the removal, and the
change would have read as a security fix while being a no-op. A test
searching for the attribute form, or stripping comments first, would pass
straight over it.
Five tests. The first is a positive control asserting the parser CAN find
the annotation where it legitimately remains (getSyncStatus) — without it,
a typo in the regex or the docblock walk would make every absence assertion
pass over an empty array and the file would be green while asserting
nothing.
exportArchiMate is pinned in BOTH directions, because the two pull opposite
ways: it must KEEP the annotation, since verifyOrgExportPermission grants
organisation-admins and removing it would drop that tier, AND it must call
that helper. The right fix for its neighbours was the wrong fix for it.
getSbomImportStatus is pinned for guard presence and for guard POSITION —
inside the try, because authorizeRead() reaches ObjectService::find(), which
re-throws for an unknown uuid, so guarding ahead of the try would turn a
clean 404 into a 500 for exactly the callers the guard was added for.
Proven able to fail, prediction written first: re-planting the prose form of
the annotation reddens exactly testAdminActionsDoNotDeclareNoAdminRequired
and nothing else. Reverted byte-identically.
The positional assertion first failed on its own account, and the reason is
worth keeping: it anchored on the bare name, which the controller's own
explanatory comment uses ABOVE the try. A positional assertion over a corpus
that includes prose measures the prose. It anchors on the call now.1 parent ee2152d commit bd9751a
5 files changed
Lines changed: 449 additions & 7 deletions
File tree
- lib/Controller
- tests/Unit/Controller
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
291 | 318 | | |
292 | 319 | | |
293 | 320 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
111 | 124 | | |
112 | 125 | | |
113 | 126 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
287 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
288 | 301 | | |
289 | 302 | | |
290 | 303 | | |
| |||
299 | 312 | | |
300 | 313 | | |
301 | 314 | | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
302 | 371 | | |
303 | 372 | | |
304 | 373 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
827 | 827 | | |
828 | 828 | | |
829 | 829 | | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
830 | 835 | | |
831 | 836 | | |
832 | 837 | | |
| |||
910 | 915 | | |
911 | 916 | | |
912 | 917 | | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
913 | 922 | | |
914 | 923 | | |
915 | 924 | | |
| |||
1591 | 1600 | | |
1592 | 1601 | | |
1593 | 1602 | | |
1594 | | - | |
| 1603 | + | |
| 1604 | + | |
1595 | 1605 | | |
1596 | 1606 | | |
1597 | 1607 | | |
| 1608 | + | |
| 1609 | + | |
| 1610 | + | |
| 1611 | + | |
| 1612 | + | |
| 1613 | + | |
| 1614 | + | |
| 1615 | + | |
| 1616 | + | |
| 1617 | + | |
| 1618 | + | |
1598 | 1619 | | |
1599 | 1620 | | |
1600 | 1621 | | |
| |||
1771 | 1792 | | |
1772 | 1793 | | |
1773 | 1794 | | |
| 1795 | + | |
| 1796 | + | |
| 1797 | + | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
1774 | 1802 | | |
1775 | 1803 | | |
1776 | 1804 | | |
| |||
1868 | 1896 | | |
1869 | 1897 | | |
1870 | 1898 | | |
1871 | | - | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
| 1905 | + | |
1872 | 1906 | | |
1873 | 1907 | | |
1874 | 1908 | | |
| |||
2083 | 2117 | | |
2084 | 2118 | | |
2085 | 2119 | | |
2086 | | - | |
| 2120 | + | |
| 2121 | + | |
| 2122 | + | |
| 2123 | + | |
| 2124 | + | |
| 2125 | + | |
2087 | 2126 | | |
2088 | 2127 | | |
2089 | 2128 | | |
| |||
2126 | 2165 | | |
2127 | 2166 | | |
2128 | 2167 | | |
2129 | | - | |
| 2168 | + | |
| 2169 | + | |
| 2170 | + | |
| 2171 | + | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
2130 | 2176 | | |
2131 | 2177 | | |
2132 | 2178 | | |
| |||
2185 | 2231 | | |
2186 | 2232 | | |
2187 | 2233 | | |
2188 | | - | |
| 2234 | + | |
| 2235 | + | |
| 2236 | + | |
2189 | 2237 | | |
2190 | 2238 | | |
2191 | 2239 | | |
| |||
2228 | 2276 | | |
2229 | 2277 | | |
2230 | 2278 | | |
2231 | | - | |
| 2279 | + | |
| 2280 | + | |
| 2281 | + | |
2232 | 2282 | | |
2233 | 2283 | | |
2234 | 2284 | | |
| |||
3448 | 3498 | | |
3449 | 3499 | | |
3450 | 3500 | | |
3451 | | - | |
| 3501 | + | |
| 3502 | + | |
| 3503 | + | |
| 3504 | + | |
| 3505 | + | |
| 3506 | + | |
| 3507 | + | |
| 3508 | + | |
| 3509 | + | |
| 3510 | + | |
| 3511 | + | |
| 3512 | + | |
| 3513 | + | |
| 3514 | + | |
3452 | 3515 | | |
3453 | 3516 | | |
3454 | 3517 | | |
| |||
0 commit comments