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]