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.

derived aws emits CanExfiltrate

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 typeIdentity
target typeNoSQLDatabase
source?principal
target?ledger
permissionsqldb:ExportJournalToS3 iam:PassRole
conditionsiam_permission role_compatibility
state logicif 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."
move · open · esc close