aws-qldb-export-journal-exfiltrate
qldb:ExportJournalToS3 + iam:PassRole (for a role with s3:PutObject) allows the caller to exfiltrate the entire QLDB journal - all committed document revisions across all tables - to an S3 bucket, including cross-account buckets.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'principal': None}
HasPermission {'ledger': None}
{'principal': None} CanPassIdentity {'role': None}
where
node_type(?ledger) ==
NoSQLDatabase
?ledger.provider_type == 'AWS::QLDB::Ledger'
effective_action(?principal, ?ledger) includes 'qldb:ExportJournalToS3'
?role.trust_policy includes 'qldb.amazonaws.com' as a trusted principal
effective_action(?principal, ?role) includes 'iam:PassRole'
iam:PassRole for ?role permits iam:PassedToService including 'qldb.amazonaws.com' (or unconditioned)
effective_action(?principal, ?role) OR effective_action(?role, '*') includes 's3:PutObject' on at least one bucket
emit
| source type | Identity |
|---|---|
| target type | NoSQLDatabase |
| source | ?principal |
| target | ?ledger |
| permissions | qldb:ExportJournalToS3 iam:PassRole |
| conditions | iam_permission role_compatibility |
| state logic | if effective 'qldb:ExportJournalToS3' on ?ledger AND ?principal CanPassIdentity ?role AND ?role trusts qldb.amazonaws.com AND effective 's3:PutObject' on ?role: if the s3:PutObject scope is confirmed to be cross-account OR attacker-controlled: ACTIVE — data leaves the account boundary. elif the s3:PutObject scope is confirmed same-account and under normal S3 ACLs/policy: do NOT emit CanExfiltrate (data stays in-account). The exported journal objects are now S3 objects; their read access is governed by aws:s3 CanReadData rules (cross-reference the S3 bucket node and the role's s3:GetObject permission). elif the target bucket/account destination is unknown: CONDITIONAL(resource_policy) — gate on whether the S3 bucket policy or ACL permits access from outside (cross-account write scope). else: do not emit. |
Narrative
{principal.name} can trigger a QLDB journal export (qldb:ExportJournalToS3) on {ledger.name} and pass a service role (iam:PassRole) that QLDB assumes to write all journal blocks - including every committed document revision across all tables - to an S3 bucket. If the target bucket is attacker-controlled or cross-account, this constitutes complete data exfiltration of the entire {ledger.name} ledger.
Raw rule rules/derived/aws/qldb.yaml
id: aws-qldb-export-journal-exfiltrate
emits: CanExfiltrate
description: "qldb:ExportJournalToS3 + iam:PassRole (for a role with s3:PutObject) allows the caller to\
\ exfiltrate the entire QLDB journal \u2014 all committed document revisions across all tables \u2014\
\ to an S3 bucket, including cross-account buckets."
match:
- - principal: null
- HasPermission
- ledger: null
- - principal: null
- CanPassIdentity
- role: null
where:
- node_type(?ledger) == NoSQLDatabase
- ?ledger.provider_type == 'AWS::QLDB::Ledger'
- effective_action(?principal, ?ledger) includes 'qldb:ExportJournalToS3'
- ?role.trust_policy includes 'qldb.amazonaws.com' as a trusted principal
- effective_action(?principal, ?role) includes 'iam:PassRole'
- iam:PassRole for ?role permits iam:PassedToService including 'qldb.amazonaws.com' (or unconditioned)
- effective_action(?principal, ?role) OR effective_action(?role, '*') includes 's3:PutObject' on at least
one bucket
emit:
source_type: Identity
target_type: NoSQLDatabase
source: ?principal
target: ?ledger
permissions:
- qldb:ExportJournalToS3
- iam:PassRole
conditions:
- iam_permission
- role_compatibility
state_logic: "if effective 'qldb:ExportJournalToS3' on ?ledger AND ?principal CanPassIdentity ?role\
\ AND ?role trusts qldb.amazonaws.com AND effective 's3:PutObject' on ?role:\n if the s3:PutObject\
\ scope is confirmed to be cross-account OR attacker-controlled:\n ACTIVE \u2014 data leaves the\
\ account boundary.\n elif the s3:PutObject scope is confirmed same-account and under normal S3 ACLs/policy:\n\
\ do NOT emit CanExfiltrate (data stays in-account). The exported journal objects\n are now\
\ S3 objects; their read access is governed by aws:s3 CanReadData rules\n (cross-reference the\
\ S3 bucket node and the role's s3:GetObject permission).\n elif the target bucket/account destination\
\ is unknown:\n CONDITIONAL(resource_policy) \u2014 gate on whether the S3 bucket policy or ACL\n\
\ permits access from outside (cross-account write scope).\nelse: do not emit."
confidence: contributing_confidence * 0.90
derived_from:
- ?principal CanPassIdentity ?role
- qldb:ExportJournalToS3 EFFECTIVE on ?ledger
false_positive_note: "Requires BOTH qldb:ExportJournalToS3 on the ledger AND iam:PassRole on a role\
\ that trusts qldb.amazonaws.com AND the role has s3:PutObject on at least one bucket. A role passable\
\ only to other services (e.g., ec2.amazonaws.com) does NOT enable QLDB export \u2014 honor iam:PassedToService\
\ conditions and role trust policy principal. The export is asynchronous; it does not immediately\
\ transfer data (QLDB queues the job). However, once the job runs, the export is complete and immutable\
\ from S3's perspective. Do not emit ACTIVE when only one or two of the three required conditions\
\ are present. When the target bucket is confirmed cross-account or attacker-controlled, this is ACTIVE\
\ exfiltration; when the bucket/account destination is unknown, emit CONDITIONAL(resource_policy).\
\ When the target bucket is confirmed same-account with normal S3 ACLs/policy, do NOT emit CanExfiltrate\
\ \u2014 the data stays in-account. The exported journal objects are S3 objects; their read access\
\ is governed by S3 rules (aws:s3 CanReadData). Cross-reference the S3 bucket node and the role's\
\ s3:GetObject permission to determine whether CanReadData applies to the exported data. Note: qldb:ExportJournalToS3\
\ is scoped to a time range \u2014 a full-history export requires specifying a range covering ledger\
\ creation to present. Partial-range exports are still CanExfiltrate for the data in that range."
narrative: "{principal.name} can trigger a QLDB journal export (qldb:ExportJournalToS3) on {ledger.name}\
\ and pass a service role (iam:PassRole) that QLDB assumes to write all journal blocks \u2014 including\
\ every committed document revision across all tables \u2014 to an S3 bucket. If the target bucket\
\ is attacker-controlled or cross-account, this constitutes complete data exfiltration of the entire\
\ {ledger.name} ledger."