[ 
https://issues.apache.org/jira/browse/SPARK-58658?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

L. C. Hsieh updated SPARK-58658:
--------------------------------
    Description: 
The Spark Connect Config RPC hands back any configuration key the session 
holds, including keys that Spark treats as sensitive everywhere else. For 
example the pre-shared authentication token:

  spark.conf.get("spark.connect.authenticate.token")   // returns the token

Keys reach the session config because SQLConf.mergeSparkConf copies every 
SparkConf entry -- static ones included -- into it, and 
SparkConnectConfigHandler's read paths (handleGet, handleGetOption, 
handleGetWithDefault, handleGetAll) apply no denylist. RuntimeConfig guards 
writes to static configs with requireNonStaticConf, but reads are unguarded. So 
any secret that ends up in the driver's SparkConf is readable by a Connect 
client, both keyed and through GetAll.

 This is broader than the authentication token. Deployments that layer their 
own configuration into SparkConf expose their own secrets the same way. Apache 
Kyuubi, for instance, passes its engine configuration through --conf and 
prefixes non-spark. keys with spark., so deployment-wide secrets -- the 
ZooKeeper digest used for discovery-namespace registration, and the pre-shared 
secret behind its internal engine tokens -- land in the driver's SparkConf and 
are readable through the Config RPC (verified on 4.0.3 and 4.2.0, with the keys 
Kyuubi produces and its redaction pattern set).

None of this is a vulnerability in the client-to-server model Spark documents: 
a client that can call the Config RPC has already authenticated, and the 
authentication token is a credential it had to present to connect. It matters 
to deployments that reuse the token, or other secrets, as a value shared 
between components while end users authenticate by other means -- a proxy in 
front of Spark Connect, or the Kyuubi engine model.

There, any client the deployment admits can read a secret out of the driver and 
reach a component directly. Spark does not promise otherwise, so this is 
defense in depth rather than a fix for a Spark vulnerability; a server not 
disclosing configuration it already considers sensitive is the better default 
regardless.

Proposed change: have the Config RPC read paths withhold any key whose name 
matches the existing spark.redaction.regex 
(default(?i)secret|password|token|access[.]?key), the same pattern Spark 
already uses to redact configuration in the UI and logs. A withheld key is 
reported the way an unset key is: Get and GetOption return no value, 
GetWithDefault returns the caller's default, and GetAll omits it. 
spark.connect.authenticate.token matches this pattern, so the token case is 
covered as one instance of the general rule. Set/Unset already reject static 
configs via requireNonStaticConf, and IsModifiable returns false for one 
without disclosing anything.

Note for whoever implements this: handleGetAll strips the requested prefix from 
the keys it returns, so the match has to run on the full key before the prefix 
is removed.

  was:
The Spark Connect Config RPC hands back any configuration key the session 
holds, including {{spark.connect.authenticate.token}}:

{code}
spark.conf.get("spark.connect.authenticate.token")   // returns the token
{code}

The key reaches the session config because {{SQLConf.mergeSparkConf}} copies 
every {{SparkConf}} entry -- static ones included -- into it, and 
{{SparkConnectConfigHandler}}'s read paths ({{handleGet}}, {{handleGetOption}}, 
{{handleGetWithDefault}}, {{handleGetAll}}) apply no denylist. 
{{RuntimeConfig}} guards *writes* to static configs with 
{{requireNonStaticConf}}, but reads are unguarded.

This is not a vulnerability in the client-to-server deployment Spark itself 
documents: the token is a client credential, and a client that can call the 
Config RPC has already presented it, so reading it back discloses nothing.

It does matter for deployments that place a proxy in front of Spark Connect and 
reuse this config as a secret shared between the proxy and the servers, with 
end users authenticating to the proxy by other means. There, any user the proxy 
admits can read the token out of a backend and then connect to that backend 
directly, bypassing whatever the proxy enforces. That reinterpretation of the 
config is the deployment's own, not something Spark promises -- which is why 
this is filed as an improvement rather than a security issue -- but a server 
not disclosing its own credential is the better default regardless, and it is 
cheap.

*Proposed change:* report the key as unset on every read path: {{Get}} and 
{{GetOption}} return no value, {{GetWithDefault}} returns the caller's default, 
and {{GetAll}} omits it. {{Set}}/{{Unset}} already reject it via 
{{requireNonStaticConf}}, and {{IsModifiable}} returns false for a static 
config without disclosing anything.

Note for anyone implementing this: {{handleGetAll}} strips the requested prefix 
from the keys it returns, so the filter has to run on the full key before the 
prefix is removed.



> Do not return redacted configurations from the Config RPC
> ---------------------------------------------------------
>
>                 Key: SPARK-58658
>                 URL: https://issues.apache.org/jira/browse/SPARK-58658
>             Project: Spark
>          Issue Type: Bug
>          Components: Connect
>    Affects Versions: 4.2.0, 4.1.2, 4.3.0
>            Reporter: L. C. Hsieh
>            Assignee: L. C. Hsieh
>            Priority: Major
>              Labels: pull-request-available
>
> The Spark Connect Config RPC hands back any configuration key the session 
> holds, including keys that Spark treats as sensitive everywhere else. For 
> example the pre-shared authentication token:
>   spark.conf.get("spark.connect.authenticate.token")   // returns the token
> Keys reach the session config because SQLConf.mergeSparkConf copies every 
> SparkConf entry -- static ones included -- into it, and 
> SparkConnectConfigHandler's read paths (handleGet, handleGetOption, 
> handleGetWithDefault, handleGetAll) apply no denylist. RuntimeConfig guards 
> writes to static configs with requireNonStaticConf, but reads are unguarded. 
> So any secret that ends up in the driver's SparkConf is readable by a Connect 
> client, both keyed and through GetAll.
>  This is broader than the authentication token. Deployments that layer their 
> own configuration into SparkConf expose their own secrets the same way. 
> Apache Kyuubi, for instance, passes its engine configuration through --conf 
> and prefixes non-spark. keys with spark., so deployment-wide secrets -- the 
> ZooKeeper digest used for discovery-namespace registration, and the 
> pre-shared secret behind its internal engine tokens -- land in the driver's 
> SparkConf and are readable through the Config RPC (verified on 4.0.3 and 
> 4.2.0, with the keys Kyuubi produces and its redaction pattern set).
> None of this is a vulnerability in the client-to-server model Spark 
> documents: a client that can call the Config RPC has already authenticated, 
> and the authentication token is a credential it had to present to connect. It 
> matters to deployments that reuse the token, or other secrets, as a value 
> shared between components while end users authenticate by other means -- a 
> proxy in front of Spark Connect, or the Kyuubi engine model.
> There, any client the deployment admits can read a secret out of the driver 
> and reach a component directly. Spark does not promise otherwise, so this is 
> defense in depth rather than a fix for a Spark vulnerability; a server not 
> disclosing configuration it already considers sensitive is the better default 
> regardless.
> Proposed change: have the Config RPC read paths withhold any key whose name 
> matches the existing spark.redaction.regex 
> (default(?i)secret|password|token|access[.]?key), the same pattern Spark 
> already uses to redact configuration in the UI and logs. A withheld key is 
> reported the way an unset key is: Get and GetOption return no value, 
> GetWithDefault returns the caller's default, and GetAll omits it. 
> spark.connect.authenticate.token matches this pattern, so the token case is 
> covered as one instance of the general rule. Set/Unset already reject static 
> configs via requireNonStaticConf, and IsModifiable returns false for one 
> without disclosing anything.
> Note for whoever implements this: handleGetAll strips the requested prefix 
> from the keys it returns, so the match has to run on the full key before the 
> prefix is removed.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to