mattcasters opened a new issue, #8888:
URL: https://github.com/apache/hop/issues/8888

   ### Apache Hop version?
   
   2.20.0-SNAPSHOT (main `5f6f681715`, 2026-10-12). The authorization filter is 
unchanged since it landed in `705fbfb036` (2026-08-29, #8169). Confirmed 
identical to the tree at `6ab2e1acd5`.
   
   ### Java version?
   
   21
   
   ### Operating system
   
   Linux. Hop Web runs on `tomcat:10-jdk21` (`docker/web.Dockerfile`), deployed 
as `ROOT`. Reproduced on embedded Apache Tomcat 10.1.60, which is the current 
Tomcat 10.1 line that image tracks. `docker/resources/server.xml` does not set 
`rejectSuspiciousURIs`.
   
   ### What happened?
   
   This blocks the 2.20.0 release.
   
   `HopServerAuthorizationFilter` is supposed to default-deny `/hop/*` in mode 
`NONE` and to enforce per-endpoint permissions in `BASIC` / `OAUTH2` / 
`EXTERNAL` (THREAT_MODEL.md §8). A request whose path is not in canonical form 
skips that gate and still reaches `HopServerServlet`.
   
   The filter takes the path from raw `getRequestURI()` (`pathWithinApp`, 
`HopServerAuthorizationFilter.java` around lines 192-205). If that string does 
not start with `/hop`, it calls `chain.doFilter` and returns, before the 
mode-`NONE` 403 and before the permission check (lines 93-98). That is 
fail-open.
   
   Tomcat collapses `//` and resolves `/./` and `/../` on the URI it uses for 
servlet and filter mapping. `getRequestURI()` stays the original request line. 
`getServletPath()` and `getPathInfo()` come from the normalized mapping. The 
filter is mapped to `/hop/*`, so it does run, sees a raw path that does not 
start with `/hop`, and lets the request through. `HopServerServlet.doGet` then 
dispatches on `getPathInfo()` (line 88). Plugins are registered with 
`setJettyMode(false)`, so the `getRequestURI().startsWith("/hop/...")` guards 
inside the individual servlets do not run.
   
   Reproduced with the same `pathWithinApp` check and a servlet mapped to 
`/hop/*`:
   
   | Request | Filter | What the servlet receives |
   | --- | --- | --- |
   | `/hop/execPipeline` | denied (starts with `/hop`) | not reached |
   | `//hop/execPipeline` | fail-open | `servletPath=/hop`, 
`pathInfo=/execPipeline` |
   | `/./hop/execPipeline` | fail-open | same |
   | `/foo/../hop/execPipeline` | fail-open | same |
   | `/%2e/hop/execPipeline` | fail-open | same |
   | `/;/hop/execPipeline` | fail-open | same |
   
   `CrossSiteRequestFilter` is also mapped on `/hop/*` and allows clients that 
send no `Sec-Fetch-*` headers, so it does not stop this.
   
   **Modes**
   
   - `NONE` is the default, and `allowUnauthenticatedServerApi` defaults to 
false. The fail-open happens before that check, so the paths above are 
unauthenticated. The endpoints reached include the ones that register and 
execute pipelines (`execPipeline`, `registerPipeline`, `prepareExec`, 
`startExec`, `webService`, `addExport`).
   - `BASIC` and `OAUTH2`: `HopBasicAuthFilter` is mapped to `/*` and still 
requires a login. The authorization filter then skips the role check, so a 
read-only user reaches execute endpoints. `READ_ONLY` has `FILE_VIEW` only 
(`HopRole`).
   - `EXTERNAL`: the same fail-open skips the 401 that a missing principal 
would otherwise get. Shipped `web.xml` has no `<security-constraint>`.
   
   **Second form, authenticated modes only**
   
   A path that already starts with `/hop` is not fail-open. 
`HopServerEndpointPermissionMapper.normalize()` only strips a semicolon, a 
query string, and a trailing slash. It does not resolve `.` or `..`, and the 
permission table uses longest prefix. On Tomcat 10.1.60:
   
   - `/hop/status/../execPipeline` is classified as `/hop/status` (`FILE_VIEW`, 
granted to `READ_ONLY`) while the servlet receives `pathInfo=/execPipeline`.
   - `/hop/status;x=1/../execPipeline` is the same: the mapper cuts at the 
first `;` and sees `/hop/status`.
   
   In mode `NONE` those two still start with `/hop`, so the closed-by-default 
branch returns 403. They matter for `BASIC` / `OAUTH2`.
   
   These are denied today and should stay denied: `/hop/./execPipeline`, 
`/hop/foo/../execPipeline`, `/hop//execPipeline`, `/hop/%2e/execPipeline`. They 
start with `/hop` and match no permission, so default-deny returns 403 before 
the servlet runs.
   
   `HopServerAuthorizationFilterTest` only sends already-normalized `/hop/...` 
URIs.
   
   The 2.19.0 release has no such filter at all. That half was already reported 
to the Apache security list on 2026-09-14. This issue is the bypass of the 2.20 
fix, which is not in any release yet.
   
   ### Fix
   
   Authorize the path the servlet will dispatch, `getServletPath()` plus 
`getPathInfo()`, which the container has already normalized. Do not fail open 
when the raw URI does not start with `/hop`. Deny when the raw path and the 
mapped path are not the same endpoint, including when a read-endpoint prefix 
such as `/hop/status` is followed by `/../` or a matrix parameter. 
`HopServerEndpointPermissionMapper` must not treat a path that still contains 
`.` or `..` as a prefix of a different endpoint.
   
   Add filter tests that supply both the raw `getRequestURI()` and the mapped 
`getServletPath()` / `getPathInfo()`:
   
   - mode `NONE`: `//hop/execPipeline`, `/./hop/execPipeline`, 
`/foo/../hop/execPipeline`, `/%2e/hop/execPipeline`, `/;/hop/execPipeline` → 
403, chain not called
   - mode `BASIC`, read-only principal: `/hop/status/../execPipeline` and 
`/hop/status;x=1/../execPipeline` with `pathInfo=/execPipeline` → 403
   - canonical `/hop/execPipeline` stays 403 in mode `NONE` and for a read-only 
user, and stays allowed for a role that has `RUN_EXECUTE`
   
   ### Issue Priority
   
   Priority: 0
   
   ### Issue Component
   
   Component: Hop Web
   Component: Hop Server
   


-- 
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]

Reply via email to