gcp-datastream-connection-profile-exposes-cred
Datastream ConnectionProfile resources store source DB credentials (username/password or SSH private key). An attacker with write access (connectionProfiles.create/update) can harvest credentials by redirecting a test connection to a controlled host; secretManagerStoredPassword references expose Secret Manager paths.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
Secret (or ConnectionString generic_type with class Secret)
?connectionProfile has credential-type fields: oracleProfile | mysqlProfile | postgresqlProfile | sqlServerProfile with username set, OR forwardSshConnectivity.privateKey set, OR secretManagerStoredPassword set
emit
| source type | Compute |
|---|---|
| target type | Credential |
| source | <Datastream service compute (Datastream execution context)> |
| target | <source database credential (Credential / Secret / AccessKey)> |
| permissions | datastream.connectionProfiles.create datastream.connectionProfiles.update |
| conditions | iam_permission credential_harvesting |
| state logic | CONDITIONAL(credential_harvesting) by default: the ConnectionProfile stores server-side encrypted credentials that are NOT returned by the GET API. The attack path requires the principal to have one of: (a) ACTIVE(datastream.connectionProfiles.create or .update) to create/update a profile pointing to an attacker-controlled host, triggering a connection test that harvests the credentials; or (b) ACTIVE(datastream.connectionProfiles.get) + ACTIVE(secretmanager.versions.access) if secretManagerStoredPassword is set (reveals SM path). For case (a), emit ACTIVE when the principal has write access and the connection test would complete (network reachability confirmed). For case (b), the credential-chains linchpin (read-secret-yields-identity) handles the chain; this rule surfaces the ExposesCredential structural fact. |
Narrative
An attacker with datastream.connectionProfiles.create or .update can modify the Datastream ConnectionProfile {connectionProfile.name} to test connectivity to an attacker-controlled database host. During the connection test, Datastream sends the stored source database credentials (username + password, or SSH key) to that host, allowing credential harvesting. Alternatively, if the profile references secretManagerStoredPassword, the Secret Manager path is exposed in the connection profile and can be read via secretmanager.versions.access.
Raw rule rules/derived/gcp/datastream.yaml
id: gcp-datastream-connection-profile-exposes-cred
emits: ExposesCredential
description: Datastream ConnectionProfile resources store source DB credentials (username/password or
SSH private key). An attacker with write access (connectionProfiles.create/update) can harvest credentials
by redirecting a test connection to a controlled host; secretManagerStoredPassword references expose
Secret Manager paths.
match:
- - ?principal
- CanModifyConfiguration
- ?connectionProfile
where:
- node_type(?connectionProfile) == Secret (or ConnectionString generic_type with class Secret)
- '?connectionProfile has credential-type fields: oracleProfile | mysqlProfile | postgresqlProfile | sqlServerProfile
with username set, OR forwardSshConnectivity.privateKey set, OR secretManagerStoredPassword set'
emit:
source_type: Compute
target_type: Credential
source: <Datastream service compute (Datastream execution context)>
target: <source database credential (Credential / Secret / AccessKey)>
permissions:
- datastream.connectionProfiles.create
- datastream.connectionProfiles.update
conditions:
- iam_permission
- credential_harvesting
state_logic: 'CONDITIONAL(credential_harvesting) by default: the ConnectionProfile stores server-side
encrypted credentials that are NOT returned by the GET API. The attack path requires the principal
to have one of: (a) ACTIVE(datastream.connectionProfiles.create or .update) to create/update a profile
pointing to an attacker-controlled host, triggering a connection test that harvests the credentials;
or (b) ACTIVE(datastream.connectionProfiles.get) + ACTIVE(secretmanager.versions.access) if secretManagerStoredPassword
is set (reveals SM path). For case (a), emit ACTIVE when the principal has write access and the connection
test would complete (network reachability confirmed). For case (b), the credential-chains linchpin
(read-secret-yields-identity) handles the chain; this rule surfaces the ExposesCredential structural
fact.'
confidence: 0.65
derived_from:
- <CanModifyConfiguration edge on ?connectionProfile>
- <ConnectionProfile credential fields>
false_positive_note: "The `password` field is input-only and NOT returned by connectionProfiles.get.\
\ Do NOT emit ACTIVE(principal, credential) based on read-only GET access. Credential exposure requires\
\ WRITE access (create/update) to bounce the connection test to an attacker host, OR a secretManagerStoredPassword\
\ reference combined with Secret Manager read access (handled by credential-chains, not re-derived\
\ here). SSH private key fields are also redacted in API responses \u2014 only the presence of the\
\ key (stored server-side) is confirmed. Connection testing may fail if network reachability to the\
\ test host is blocked."
narrative: An attacker with datastream.connectionProfiles.create or .update can modify the Datastream
ConnectionProfile {connectionProfile.name} to test connectivity to an attacker-controlled database
host. During the connection test, Datastream sends the stored source database credentials (username
+ password, or SSH key) to that host, allowing credential harvesting. Alternatively, if the profile
references secretManagerStoredPassword, the Secret Manager path is exposed in the connection profile
and can be read via secretmanager.versions.access.