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.

derived gcp emits ExposesCredential

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

?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 typeCompute
target typeCredential
source<Datastream service compute (Datastream execution context)>
target<source database credential (Credential / Secret / AccessKey)>
permissionsdatastream.connectionProfiles.create datastream.connectionProfiles.update
conditionsiam_permission credential_harvesting
state logicCONDITIONAL(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.
move · open · esc close