gcp-spanner-exfiltrate-via-backup
Creating a Spanner backup (spanner.backups.create) then setting the backup's IAM policy (spanner.backups.setIamPolicy) to grant a cross-project principal spanner.backups.restoreDatabase copies the full database contents across the project boundary - effective exfiltration.
match (effective permission)
{
"action": "spanner.backups.create",
"resource_type": "google.spanner.admin.instance.v1.Instance"
}
where
?principal has EFFECTIVE spanner.databases.get on the source database (required to read the database for backup creation)
?principal has EFFECTIVE spanner.backups.setIamPolicy on the instance or project scope (applies to backups created within that scope)
?principal (or an identity they control in another project) can call spanner.databases.create and spanner.backups.restoreDatabase to restore the backup
emit
| source type | Identity |
|---|---|
| target type | RelationalDatabase |
| source | <principal> |
| target | <Spanner Database node> |
| permissions | spanner.backups.create spanner.backups.setIamPolicy |
| conditions | iam_permission resource_policy |
| state logic | ACTIVE when spanner.backups.create on the source database and spanner.backups.setIamPolicy on the instance or project scope are both confirmed ACTIVE and the attacker controls a restore target (another project with a database + spanner.backups.restoreDatabase grant). CONDITIONAL(iam_permission) when setIamPolicy on backups is uncertain (e.g., granted only at database scope but not instance/project scope, which may not propagate to a newly created backup). CONDITIONAL(resource_policy) when the cross-project restore principal is unknown or cannot be confirmed. POTENTIAL when the attacker is assumed to control an external project but it is not modeled. BLOCKED when VPC-SC perimeter blocks the Spanner API or when an org policy (e.g. constraints/gcp.resourceLocations) disallows backup creation in the required region. |
Narrative
{principal.name} can create a backup of database {database.name} (spanner.backups.create) and set the backup's IAM policy (spanner.backups.setIamPolicy) to grant a cross-project principal restore access, copying the full database contents across the project boundary.
Raw rule rules/derived/gcp/spanner.yaml
id: gcp-spanner-exfiltrate-via-backup
emits: CanExfiltrate
description: "Creating a Spanner backup (spanner.backups.create) then setting the backup's IAM policy\
\ (spanner.backups.setIamPolicy) to grant a cross-project principal spanner.backups.restoreDatabase\
\ copies the full database contents across the project boundary \u2014 effective exfiltration."
match_effective_permission:
action: spanner.backups.create
resource_type: google.spanner.admin.instance.v1.Instance
where:
- ?principal has EFFECTIVE spanner.databases.get on the source database (required to read the database
for backup creation)
- ?principal has EFFECTIVE spanner.backups.setIamPolicy on the instance or project scope (applies to backups
created within that scope)
- ?principal (or an identity they control in another project) can call spanner.databases.create and spanner.backups.restoreDatabase
to restore the backup
emit:
source_type: Identity
target_type: RelationalDatabase
source: <principal>
target: <Spanner Database node>
permissions:
- spanner.backups.create
- spanner.backups.setIamPolicy
conditions:
- iam_permission
- resource_policy
state_logic: ACTIVE when spanner.backups.create on the source database and spanner.backups.setIamPolicy
on the instance or project scope are both confirmed ACTIVE and the attacker controls a restore target
(another project with a database + spanner.backups.restoreDatabase grant). CONDITIONAL(iam_permission)
when setIamPolicy on backups is uncertain (e.g., granted only at database scope but not instance/project
scope, which may not propagate to a newly created backup). CONDITIONAL(resource_policy) when the cross-project
restore principal is unknown or cannot be confirmed. POTENTIAL when the attacker is assumed to control
an external project but it is not modeled. BLOCKED when VPC-SC perimeter blocks the Spanner API or
when an org policy (e.g. constraints/gcp.resourceLocations) disallows backup creation in the required
region.
false_positive_note: "CRITICAL: spanner.backups.create requires BOTH spanner.backups.create (on instance\
\ scope) AND spanner.databases.get (on the source database). Without spanner.databases.get on the\
\ source database, the CreateBackup API call fails with PERMISSION_DENIED. Roles are critical here:\
\ roles/spanner.backupAdmin grants backups.create + backups.setIamPolicy but NOT databases.get. Only\
\ roles/spanner.admin or roles/spanner.databaseAdmin grant databases.get. A principal with only backupAdmin\
\ cannot create backups. Confirm the attacker holds ALL THREE permissions: spanner.backups.create\
\ (instance), spanner.databases.get (database), and spanner.backups.setIamPolicy (instance or project).\
\ spanner.backups.create also requires the source database is in state READY and the backup expiration\
\ is within the database's version retention period. Without setIamPolicy on the backup, the attacker\
\ can create the backup but cannot share it cross-project \u2014 downgrade to POTENTIAL(resource_policy)\
\ in that case. The alternative exfiltration path (Dataflow export to GCS) is modeled in /rules/derived/gcp/dataflow.yaml\
\ and cited there; do not duplicate it here. For CMEK-encrypted databases, the backup inherits CMEK\
\ encryption \u2014 the restore target project must have access to the same CMEK key to decrypt the\
\ restored database."
narrative: '{principal.name} can create a backup of database {database.name} (spanner.backups.create)
and set the backup''s IAM policy (spanner.backups.setIamPolicy) to grant a cross-project principal
restore access, copying the full database contents across the project boundary.'
derived_from:
- spanner.backups.create effective permission on Instance scope
- spanner.backups.setIamPolicy effective permission on Instance or Project scope
- spanner.databases.get effective permission on the source Database