amaannawab923 commented on PR #45515:
URL: https://github.com/apache/superset/pull/45515#issuecomment-6287228690

   ## Please do not adopt this approach — security review found two 
load-bearing controls broken
   
   A review pass since opening this draft found problems serious enough to 
report before anyone spends time on the code. Both stem from the same root 
decision — letting a contributed page claim a free-form top-level path — and I 
would rather state them than have a reviewer find them.
   
   ### 1. The host-route collision guard compares strings against rule 
templates, so every parameterised host route is unguarded
   
   `_register_extension_pages` builds its guard set as `{str(rule) for rule in 
current_app.url_map.iter_rules()}`. For a parameterised route that string is 
the *template* — `/dashboard/<dashboard_id_or_slug>/`, `/embedded/<uuid>`, 
`/api/v1/database/<pk>/schemas/`. A manifest path is always literal, so it can 
never be string-equal to one, the guard passes, and Werkzeug then prefers the 
*static* rule when matching.
   
   Reproduced with host rules registered first, in the same order 
`init_views()` runs before `init_extensions()`:
   
   ```
   GET /superset/dashboard/sales/     -> 200  extension shell   (host dashboard 
bypassed)
   GET /api/v1/database/1/schemas/    -> 200  extension shell   (host rule 
bypassed)
   GET /api/v1/chart/1                -> 308 -> extension shell
   GET /api/v1/chart/2                -> 200  host JSON         (other ids 
unaffected)
   ```
   
   The `/<path:subpath>` deep-link route compounds it: a page at 
`/api/v1/database/1/` shadows every sub-route for that id.
   
   ### 2. The same hole reaches the OAuth callback, and Werkzeug's 
slash-redirect carries the query string
   
   `/oauth-authorized/<provider>` is FAB's `AuthOAuthView` callback. A page 
declaring `path: "/oauth-authorized/google/"` passes validation (only lowercase 
letters and hyphens) and passes the collision guard for the reason above:
   
   ```
   GET /oauth-authorized/google?code=...&state=abc
     -> 308 Location: /oauth-authorized/google/?code=...&state=abc
     -> 200 extension shell, with the code in window.location.search
   ```
   
   The host callback never runs. At minimum this is a one-line install-time 
denial of service on authentication for that provider. Whether the 
authorization code is also readable by the extension's own bundle depends on 
whether the page's permission resolves to something an anonymous caller holds, 
which in turn depends on `PUBLIC_ROLE_LIKE`; that path is suspected but not 
confirmed end to end.
   
   ### 3. Routing the page through `add_view_no_menu` hands an extension 
control of a FAB `class_permission_name`
   
   `add_permissions_view` treats the registering view's `base_permissions` as 
the complete action list for that resource and deletes any permission on it 
that is not in the list. With a host view owning `Database` 
(`can_read`/`can_write`/`can_export`) and then a page view declaring `{"index": 
"read"}` on the same resource:
   
   ```
   before: ['can_export', 'can_read', 'can_write']
   after:  ['can_read', 'can_write']        # can_export deleted
   ```
   
   `can_write` survived only because a role still referenced it. Separately, 
the justification written into `view.py` is wrong: FAB grants a newly created 
permission to Admin immediately, and `SupersetSecurityManager._is_gamma_pvm` is 
default-allow, so an invented resource name reaches Gamma and Alpha on the next 
`superset init` — the opposite of what the docstring claims.
   
   ### 4. The client-side route is ungated and never learns about a server-side 
refusal
   
   `App.tsx` mounts every registered page unconditionally. The declared 
permission gates cold loads only; in-app navigation renders the component 
regardless. And all three backend refusals are logged server-side only, so a 
page the server refused still mounts in the browser — in-app navigation works, 
refresh 404s.
   
   ---
   
   ### Direction
   
   These are not independent bugs to patch. Three of the four would be 
structurally impossible if contributed pages lived under a host-owned namespace 
derived from the extension id, rather than claiming free-form top-level paths — 
which is also what the comparable systems do, and what 
`/api/v1/extensions/<publisher>/<name>/...` already does for extension assets 
in this codebase.
   
   I am reworking the design along those lines rather than patching, and will 
update this PR when it is ready. Independently of the namespace question, the 
collision check should ask the url_map whether a path *matches* 
(`MapAdapter.match`, treating `RequestRedirect` as a match) rather than 
comparing strings, and the path pattern should be ASCII-only — `str.isalnum()` 
is Unicode-aware, so fullwidth and Roman-numeral homoglyphs of host paths are 
currently admitted.
   
   Not marking this ready for review until that is done.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to