github-actions[bot] commented on code in PR #69588:
URL: https://github.com/apache/doris/pull/69588#discussion_r4372968032


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java:
##########
@@ -345,41 +350,97 @@ private void 
checkDriverUrlsAgainstOperatorGate(Map<String, String> candidate,
         }
     }
 
-    /** One row of the driver-jar key table: which properties name the jar, 
live under which flavor key. */
-    private static final class DriverUrlKeys {
-        /** The properties whose value the connector hands to a class loader, 
documented aliases included. */
-        final List<String> urlKeys;
-        /** The key whose candidate value must be "jdbc" for the urlKeys to be 
live; null = always live. */
-        final String flavorKey;
+    /**
+     * The connector's declared security-sensitive property surface, looked up 
by catalog type WITHOUT
+     * initializing a connector (so neither gate forces a catalog to come to 
life). {@link
+     * ConnectorSecuritySurface#EMPTY} when no provider backs this type — 
exactly the state in which the
+     * connector can neither load a driver jar nor open a connection, so there 
is nothing to police.
+     */
+    // Package-private, not private: a unit test overrides it to inject a 
surface without registering a
+    // connector plugin, exactly as it exercises the gate logic in isolation.
+    ConnectorSecuritySurface securitySurface(Map<String, String> properties) {
+        return ConnectorFactory.findProvider(getType(), properties)
+                .map(ConnectorProvider::securitySurface)
+                .orElse(ConnectorSecuritySurface.EMPTY);

Review Comment:
   [P1] Preserve driver validation when the provider is unavailable
   
   A missing or rejected plugin does not remove its persisted catalogs: 
CatalogFactory deliberately registers them in degraded mode during replay. Here 
that state now returns EMPTY, so ALTER CATALOG can journal a new driver_url 
without either JdbcDriverUrlSecurity.check or the operator allowlist 
(provider-side validation also no-ops). For example, with 
jdbc_driver_secure_path restricted to a local directory, an existing JDBC 
catalog can be changed to an arbitrary HTTP jar while its provider is absent. 
After the matching plugin is restored, lazy reconstruction calls 
JdbcCatalogProperties.of(), not the statement-only rules or 
preCreateValidation; JdbcDorisConnector.resolveDriverUrl returns that HTTP URL 
unchanged and JdbcConnectorClient hands it to URLClassLoader. Thus the skipped 
check is never recovered. The former type table explicitly prevented this case, 
and the new test override fabricates a surface even for the test that claims to 
model an absent plugin. Preserve the security me
 tadata or reject sensitive edits until the provider is available; test the 
real lookup with an empty manager.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java:
##########
@@ -345,41 +350,97 @@ private void 
checkDriverUrlsAgainstOperatorGate(Map<String, String> candidate,
         }
     }
 
-    /** One row of the driver-jar key table: which properties name the jar, 
live under which flavor key. */
-    private static final class DriverUrlKeys {
-        /** The properties whose value the connector hands to a class loader, 
documented aliases included. */
-        final List<String> urlKeys;
-        /** The key whose candidate value must be "jdbc" for the urlKeys to be 
live; null = always live. */
-        final String flavorKey;
+    /**
+     * The connector's declared security-sensitive property surface, looked up 
by catalog type WITHOUT
+     * initializing a connector (so neither gate forces a catalog to come to 
life). {@link
+     * ConnectorSecuritySurface#EMPTY} when no provider backs this type — 
exactly the state in which the
+     * connector can neither load a driver jar nor open a connection, so there 
is nothing to police.
+     */
+    // Package-private, not private: a unit test overrides it to inject a 
surface without registering a
+    // connector plugin, exactly as it exercises the gate logic in isolation.
+    ConnectorSecuritySurface securitySurface(Map<String, String> properties) {
+        return ConnectorFactory.findProvider(getType(), properties)
+                .map(ConnectorProvider::securitySurface)
+                .orElse(ConnectorSecuritySurface.EMPTY);
+    }
 
-        DriverUrlKeys(String flavorKey, String... urlKeys) {
-            this.flavorKey = flavorKey;
-            this.urlKeys = Arrays.asList(urlKeys);
+    /**
+     * Applies the operator's SSRF policy (the third-party {@code 
SecurityChecker} SDK) to every
+     * user-supplied network endpoint these properties would make the FE 
connect to, rejecting an intranet
+     * target. It mirrors {@link #checkDriverUrlsAgainstOperatorGate}: the 
connector declares the endpoint
+     * keys through {@link ConnectorSecuritySurface#ssrfEndpointKeys()}, it 
runs on CREATE
+     * (through {@code checkWhenCreating}) and on the {@code !isReplay} ALTER 
path (here and in
+     * {@code validatePropertiesBeforeUpdate}) but never on replay, and it is 
a no-op unless the operator
+     * installed an SSRF checker.
+     *
+     * <p>The judgment itself is delegated to the SDK — see {@link 
SecurityChecker#checkEndpointForSsrf}.
+     * This is a point-in-time check of the endpoint supplied at CREATE/ALTER; 
it does not guard a later
+     * runtime connection (DNS rebinding, lazy metadata access) — that is a 
deliberate scope limit.
+     *
+     * <p>A connector whose property names a host to connect to declares it in 
its
+     * {@code securitySurface()} and nowhere else.
+     */
+    private void checkEndpointsAgainstSsrf(Map<String, String> candidate, 
Map<String, String> updatedProperties)
+            throws DdlException {
+        SecurityChecker checker = SecurityChecker.getInstance();
+        if (!checker.isEnabled()) {
+            return;
+        }
+        List<String> endpointKeys = 
securitySurface(candidate).ssrfEndpointKeys();
+        if (endpointKeys.isEmpty()) {
+            return;
+        }
+        // Only an ALTER that touches an endpoint key can repoint the catalog 
at a new host; a stored value
+        // was gated at its own CREATE/ALTER time. On CREATE the full property 
map is passed as both
+        // arguments, so every supplied endpoint counts as touched and is 
checked.
+        boolean touched = 
endpointKeys.stream().anyMatch(updatedProperties::containsKey);
+        if (!touched) {
+            return;
+        }
+        for (String key : endpointKeys) {
+            String rawValue = candidate.get(key);
+            if (rawValue == null || rawValue.trim().isEmpty()) {
+                continue;
+            }
+            // A value may list several comma-separated endpoints (e.g. HA 
hive.metastore.uris).
+            for (String endpoint : rawValue.split(",")) {
+                HostAndPort hostAndPort = parseEndpoint(endpoint.trim());
+                if (hostAndPort == null) {
+                    continue;
+                }
+                checker.checkEndpointForSsrf(hostAndPort.getHost(), 
hostAndPort.getPort());
+            }
         }
     }
 
     /**
-     * The driver-jar properties of the three jdbc-flavored catalog types, 
spelled out here because the
-     * fe.conf policy is the engine's to apply while the keys belong to the 
connectors, and widening the
-     * plugin SPI for three constants is not worth a plugin API major bump. 
The keys are the user-facing
-     * property names (with their documented aliases), which are wire-stable. 
This single table drives
-     * BOTH halves of the gate — the trigger set (urlKeys plus flavorKey: the 
changes that can repoint
-     * the loaded jar) and the values that get checked — so the two can never 
drift apart. Owners:
-     * JdbcCatalogProperties, IcebergJdbcMetaStoreProperties, 
PaimonJdbcMetaStoreProperties. A new
-     * consumer of a jdbc-flavored driver_url adds its row here and nowhere 
else (see the "Remote
-     * Artifacts and Dynamic Code Loading" section of AGENTS.md).
+     * Parse one endpoint — {@code scheme://host:port} (e.g. {@code 
thrift://host:9083}) or a bare
+     * {@code host:port} — into host and port for the SSRF probe. Returns null 
when no concrete host:port
+     * can be extracted (nothing to probe); {@link HostAndPort} handles IPv6 
literals in brackets natively.
      */
-    private static DriverUrlKeys driverUrlKeysOf(String catalogType) {
-        if ("jdbc".equalsIgnoreCase(catalogType)) {
-            return new DriverUrlKeys(null, "driver_url", "jdbc.driver_url");
+    static HostAndPort parseEndpoint(String value) {
+        if (value == null || value.isEmpty()) {
+            return null;
         }
-        if ("iceberg".equalsIgnoreCase(catalogType)) {
-            return new DriverUrlKeys("iceberg.catalog.type", 
"iceberg.jdbc.driver_url");
+        // Drop a leading scheme (thrift://, http://, ...) and any trailing 
path, leaving the authority.
+        String authority = value;
+        int schemeIdx = authority.indexOf("://");
+        if (schemeIdx >= 0) {
+            authority = authority.substring(schemeIdx + 3);
         }
-        if ("paimon".equalsIgnoreCase(catalogType)) {
-            return new DriverUrlKeys("paimon.catalog.type", 
"paimon.jdbc.driver_url", "jdbc.driver_url");
+        int slashIdx = authority.indexOf('/');
+        if (slashIdx >= 0) {
+            authority = authority.substring(0, slashIdx);
+        }
+        try {
+            HostAndPort hostAndPort = HostAndPort.fromString(authority);
+            if (!hostAndPort.hasPort() || hostAndPort.getHost().isEmpty()) {
+                return null;
+            }
+            return hostAndPort;
+        } catch (IllegalArgumentException e) {

Review Comment:
   [P2] Parse the same URI destination that the HMS client uses
   
   With a configured checker, 
hive.metastore.uris=thrift://127.0.0.1:9083?ignored skips the new gate: only a 
slash-delimited path is stripped, so HostAndPort receives port text 
9083?ignored, throws, and this catch returns null. checkEndpointsAgainstSsrf 
then continues without probing anything. The value is valid for the actual 
HiveMetaStoreClient: resolveUris parses java.net.URI and preserves its 
query/fragment, while open connects using only store.getHost()/getPort(), i.e. 
127.0.0.1:9083. A #fragment suffix has the same effect. Consequently ALTER 
accepts the destination the checker would reject without the suffix; CREATE 
does too with test_connection=false. Extract host/port using URI semantics for 
schemed values and add query/fragment/userinfo cases so valid client 
destinations cannot silently escape the probe.



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