morningman opened a new pull request, #70092:
URL: https://github.com/apache/doris/pull/70092

   ### What problem does this PR solve?
   
   Issue Number: None
   
   Related PR: #69588
   
   Problem Summary:
   
   Backport of #69588 to branch-4.1. The master change cannot be cherry-picked: 
it is built on the fe-connector plugin SPI (`ConnectorProvider`, 
`ConnectorSecuritySurface`, `PluginDrivenExternalCatalog`), and none of those 
exist on 4.1. This PR ports the security fix itself onto the 4.1 catalog 
classes, with the same rules and the same error messages.
   
   **In short**
   
   With the SSRF security SDK configured (`security_checker_class_name`), 
CREATE / ALTER CATALOG on 4.1 never checked an HMS catalog's metastore 
endpoint, so a user with CREATE_PRIV on a catalog (no ADMIN) could make the 
FE's HMS client connect to an intranet address. `HMSExternalCatalog` now runs 
the same CREATE / ALTER gate master got in #69588 (on ALTER, before 
`CatalogMgr` takes its write lock), and the SDK binding no longer leaks its 
net-hook onto pooled FE threads. Without a checker configured (the open-source 
default), the new gate does nothing.
   
   **Background**
   
   - `security_checker_class_name` names a third-party SDK (Aliyun 
`com.aliyun.securitysdk.SecurityUtil`), loaded by reflection through 
`SecurityChecker` / `UrlSecurityChecker` in fe-common. Its net-hook sits at the 
JDK socket layer: install it on the current thread, connect, and the connect 
throws `SSRFUnsafeConnectionException` (a `SecurityException`) if the resolved 
address is intranet. The SDK has no "is this host intranet" string API, so it 
can only judge a real connect. Today only HTTP callers use it (HTTP TVF, S3, 
ES, small-file download, stage creation).
   - An HMS catalog takes its endpoint from `hive.metastore.uris` or its alias 
`uri`. `HMSBaseProperties` sets that value, plus every other `hive.`-prefixed 
catalog property, into the `HiveConf` of the FE's HMS client. 
`HiveMetaStoreClient` reads the endpoint through `MetastoreConf.getVar`, which 
looks at `metastore.thrift.uris` first, and through Hadoop's 
`Configuration.get`, which expands `${...}` placeholders (other properties, 
`${env.X}`, JVM system properties).
   
   **1. The problem, and what it cost**
   
   - CREATE CATALOG (CREATE_PRIV) and ALTER CATALOG (ALTER privilege on the 
catalog) accepted any metastore endpoint, so the FE opened a thrift connection 
to a host inside its own network on behalf of a non-admin user.
   - `UrlSecurityChecker` bound the SDK's stop method as 
`stopSSRFNetHookChecking(String)` but invoked it with no argument, so stop 
never ran and the net-hook stayed on the pooled FE thread after every bracketed 
HTTP request.
   - Only the http form of the SDK's start was bound, so nothing could hook a 
non-http connection such as HMS thrift.
   
   **2. How this PR fixes it**
   
   - fe-common, `SecurityChecker`: adds the no-arg `startSSRFChecking()` (the 
SDK's TCP form), `isEnabled()`, and `checkEndpointForSsrf(host, port)`, which 
brackets a single 5-second TCP connect with the net-hook. A `SecurityException` 
from the connect is the rejection; a refused / timed-out / unknown-host connect 
is ignored, so a public endpoint that is down right now can still be used. 
`UrlSecurityChecker` binds the SDK's no-arg start and no-arg stop; 
`DummySecurityChecker.isEnabled()` returns false. The singleton becomes lazily 
initialized with a test setter.
   - fe-core, `HMSExternalCatalog`: `checkWhenCreating()` (CREATE, before 
`test_connection`) and the new `validatePropertiesBeforeUpdateUnlocked()` 
(ALTER) call the new gate on `hive.metastore.uris`, `uri` and 
`metastore.thrift.uris`. The rules match master: no-op without a checker; a 
value containing `${` is rejected before anything else (so an ALTER of only the 
property a stored placeholder refers to is rejected as well); an ALTER probes 
only when it touches one of these keys; every entry of a comma-separated HA 
list is probed; schemed values are parsed with `java.net.URI` host/port 
semantics, so a query, fragment or userinfo part cannot hide the real 
destination.
   - fe-core, `CatalogMgr.alterCatalogProps`: on 4.1 ALTER CATALOG is validated 
under `CatalogMgr`'s write lock, and a probe to an unreachable endpoint can 
wait up to 5 seconds, so the probe must not run there. ALTER now takes a 
snapshot of the catalog's properties under the read lock, runs 
`ExternalCatalog.validatePropertiesBeforeUpdateUnlocked` (new, a no-op by 
default) on it without holding the lock, and then applies the change under the 
write lock only if the same catalog still has the snapshot's properties; if 
another DDL changed or dropped the catalog in between, it starts over from a 
new snapshot. The rest of the ALTER validation still runs under the lock, 
unchanged. CREATE already probes before `CatalogMgr` takes its lock.
   
   Where the backport differs from master:
   
   | Master (#69588) | This PR | Why |
   | --- | --- | --- |
   | Endpoint keys declared by the hive connector through 
`ConnectorSecuritySurface` | Keys are a constant in `HMSExternalCatalog` | 4.1 
has no connector SPI; `HMSExternalCatalog` is already the HMS-specific class |
   | Gate in `PluginDrivenExternalCatalog` | Gate in `HMSExternalCatalog`, 
through `checkWhenCreating` (CREATE) and the new 
`validatePropertiesBeforeUpdateUnlocked` (ALTER) | Neither runs on replay: 
`CatalogFactory` calls `checkWhenCreating` only for a new catalog, and replay 
goes through `replayAlterCatalogProps`, not `alterCatalogProps` |
   | `driver_url` gate moved onto the security surface, `driverUrlKeysOf` 
deleted | Not ported | 4.1 checks `driver_url` in `JdbcExternalCatalog` / 
`IcebergExternalCatalog` / `PaimonExternalCatalog.checkProperties`; there is no 
key table to replace |
   | ALTER rejected while the connector provider is missing | Not ported | 4.1 
has no connector providers |
   | `checkEndpointForSsrf` throws `DdlException` | Throws `Exception` with the 
same messages; the HMS gate rethrows it as `DdlException` | On 4.1 
`DdlException` is in fe-core, which fe-common cannot reference |
   | Connector plugin API 22.0 -> 23.0 | No version change | No connector 
plugin API on 4.1 |
   | All ALTER validation runs before `CatalogMgr` takes its write lock 
(#68585) | Only the lock-free checks (the probe) run before the write lock, 
through `validatePropertiesBeforeUpdateUnlocked`, on a snapshot that is checked 
again under the lock; the rest of ALTER validation stays under the lock | 
#68585 is not on 4.1 and also changes HDFS HA validation; this keeps the change 
to the probe |
   | `metastore.thrift.uris` gated because the hive connector passes every 
property to `HiveConf` | Gated as well | `HMSBaseProperties` copies only 
`hive.`-prefixed properties today, so the key does not reach the client yet; 
gating it keeps the gate correct if that copy widens, and keeps one rule on 
both branches |
   
   **3. Results**
   
   With an SSRF checker configured:
   
   | Statement | Before | After |
   | --- | --- | --- |
   | `CREATE CATALOG ... ("type"="hms", 
"hive.metastore.uris"="thrift://10.0.0.1:9083")` | Accepted | Rejected: 
`Endpoint 10.0.0.1:9083 is rejected by the SSRF policy (intranet address not 
allowed): ...` |
   | Same, with `test_connection` = false | Accepted | Rejected (the gate does 
not depend on `test_connection`) |
   | `ALTER CATALOG c SET PROPERTIES 
("hive.metastore.uris"="thrift://10.0.0.1:9083")` | Accepted | Rejected |
   | `thrift://[email protected]:9083?x#y`, or an intranet host second in an HA 
list | Accepted | Rejected |
   | `"hive.metastore.uris"="thrift://${hive.x}:9083"` with 
`"hive.x"="127.0.0.1"` | Accepted; the client connects to 127.0.0.1 | Rejected: 
`... must give its endpoint literally while an SSRF checker is configured ...` |
   | Public endpoint that is currently unreachable | Accepted | Accepted (a 
failed connect is not an SSRF signal) |
   | ALTER of an unrelated property | Accepted | Accepted, no probe |
   | ALTER that probes an unreachable public endpoint | No probe | Probes for 
up to 5 seconds without holding `CatalogMgr`'s lock |
   | Replay of CREATE / ALTER from the edit log | No probe | No probe |
   
   Without a checker configured, nothing changes. The only change outside the 
gate is that the SDK's net-hook is now removed after each bracketed HTTP 
request, as intended.
   
   **4. The classes, and how they call each other**
   
   - `SecurityChecker` (fe-common): the SSRF primitive `checkEndpointForSsrf`, 
plus `isEnabled()` and the TCP start.
   - `UrlSecurityChecker` / `DummySecurityChecker` (fe-common): SDK binding / 
no-op.
   - `HMSExternalCatalog` (fe-core): the endpoint keys, 
`checkEndpointsAgainstSsrf`, `parseEndpoint`, and the two hooks.
   - `ExternalCatalog` (fe-core): the new 
`validatePropertiesBeforeUpdateUnlocked` hook, a no-op by default.
   - `CatalogMgr` (fe-core): `alterCatalogProps` runs that hook on a snapshot 
before taking its write lock and checks the snapshot again under it.
   
   ```
   CREATE CATALOG
     CatalogFactory.createFromCommand
      └ HMSExternalCatalog.checkWhenCreating
         ├ checkEndpointsAgainstSsrf  (new)  -> 
SecurityChecker.checkEndpointForSsrf
         └ super.checkWhenCreating  (test_connection)
   
   ALTER CATALOG ... SET PROPERTIES
     CatalogMgr.alterCatalogProps
      ├ read lock:  snapshot the catalog's properties
      ├ no lock:    HMSExternalCatalog.validatePropertiesBeforeUpdateUnlocked  
(new)
      │              └ checkEndpointsAgainstSsrf  (new)  -> 
SecurityChecker.checkEndpointForSsrf
      └ write lock: same catalog, same properties? if not, start over
                     └ alterExternalCatalogPropsFenced -> 
validatePropertiesBeforeUpdate
                        -> checkProperties (unchanged), publish, journal
   
   SecurityChecker.checkEndpointForSsrf(host, port)
    ├ startSSRFChecking()       -> SDK startSSRFNetHookChecking()
    ├ doConnect(host, port, 5s) -> SDK throws SecurityException for an intranet 
target
    └ stopSSRFChecking()        -> SDK stopSSRFNetHookChecking()
   
   Replay: CatalogFactory.createFromLog and 
replayAlterCatalogProps(isReplay=true) call neither hook.
   ```
   
   SSRF surface note (per repo policy): the probe is a new FE -> 
user-named-host outbound TCP connect, made only when the operator installed the 
SSRF SDK and only at CREATE / ALTER CATALOG (CREATE_PRIV or ADMIN for CREATE, 
ALTER privilege for ALTER). It exists only so the SDK can reject an intranet 
target. Known limits, same as master: it is a point-in-time check, so it does 
not guard a later runtime connection (DNS rebinding) or a client that bypasses 
the JDK socket layer.
   
   ### Release note
   
   When an SSRF checker is configured, CREATE / ALTER CATALOG checks HMS 
endpoint destinations (`hive.metastore.uris`, `uri`, and 
`metastore.thrift.uris`), including URIs containing query, fragment, or 
userinfo components, and rejects an HMS endpoint value that contains a `${...}` 
placeholder.
   
   ### Check List (For Author)
   
   - Test <!-- At least one of them must be included. -->
       - [ ] Regression test
       - [x] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
           - [ ] This is a refactor/code format and no logic has been changed.
           - [ ] Previous test can cover this change.
           - [ ] No code files have been changed.
           - [ ] Other reason <!-- Add your reason?  -->
   
       - Unit Test: the new `SecurityCheckerTest` (6 tests) and 
`HMSExternalCatalogSsrfGateTest` (48), plus the existing 
`HMSExternalCatalogParquetTimeZoneTest`, `HttpTableValuedFunctionTest`, 
`CatalogMgrTest`, `CatalogMgrLanceGuardTest`, `ExternalCatalogTest`, 
`ExternalCatalogRuntimeStateTest`, `CatalogVarbinaryMigrationTest`, 
`HMSPropertiesTest`, `HiveTableSinkTest`, `JdbcExternalCatalogTest` and 
`PaimonExternalCatalogDriverUrlSecurityTest`: 134 tests in 13 classes passed 
with `TZ=Asia/Shanghai mvn test -pl fe-common,fe-core -am`; Checkstyle reports 
0 violations in fe-common and fe-core.
       - `HMSExternalCatalogSsrfGateTest` also drives ALTER through a real 
`CatalogMgr`. It checks that the probe runs while the thread holds neither the 
read nor the write lock, that an intranet target leaves the catalog and the 
journal untouched, and that an ALTER whose catalog is altered or dropped during 
the probe checks again, or fails with `No catalog found`, instead of applying a 
stale check.
       - Mutation check: each of 21 mutants is killed by at least one new test. 
The mutants drop either hook call, the `isEnabled()` guard of the gate or of 
`checkEndpointForSsrf`, the touched-key guard, a declared key, the comma split 
or the URI parsing; run the placeholder rule only on touched keys or ahead of 
the `isEnabled()` guard; swallow the SDK's rejection or a failed hook install; 
treat a failed connect as a rejection; skip `stopSSRFChecking()` in `finally`; 
probe under `CatalogMgr`'s write lock (from `validatePropertiesBeforeUpdate`, 
or by calling the lock-free hook under the lock); or drop the snapshot 
comparison or the catalog identity check under the lock.
       - Checked on this branch that `HMSBaseProperties` hands 
`hive.metastore.uris` = `thrift://${hive.x}:9083` with `hive.x` = `127.0.0.1` 
to the HMS client as `thrift://127.0.0.1:9083`, and does not hand it a 
`metastore.thrift.uris` catalog property.
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. With an SSRF checker configured, CREATE / ALTER CATALOG 
probes HMS metastore endpoints and rejects intranet targets and `${...}` 
placeholders; the SDK net-hook is now removed after each bracketed request. 
Without a checker, nothing changes.
   
   - Does this need documentation?
       - [x] No.
       - [ ] Yes. <!-- Add document PR link here. eg: 
https://github.com/apache/doris-website/pull/1214 -->
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test cases
   - [ ] Confirm document
   - [ ] Add branch pick label <!-- Add branch pick label that this PR should 
merge into -->
   


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