aws-appflow-update-flow-exfil
Redirecting an existing flow's destination to an attacker-controlled endpoint and triggering the flow exfiltrates the SaaS data it reads (Salesforce records, Zendesk tickets, Slack messages, etc.).
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
node_type(?flow) ==
GenericMessaging or node_class(?flow) == Messaging
?flow is an AWS::AppFlow::Flow
?flow has a configured source (SaaS connector profile) with data — i.e. the flow reads non-empty data
optional
Present → upgrade the emitted state; absent → downgrade (not a hard requirement).
{'principal': None}
CanWriteData {'flow': None}
emit
| source type | Identity |
|---|---|
| target type | Data |
| source | ?principal |
| target | <data read by ?flow from its SaaS source (e.g. Salesforce objects, Zendesk tickets)> |
| permissions | appflow:UpdateFlow appflow:StartFlow? |
| conditions | iam_permission resource_policy trigger_exists |
| state logic | ACTIVE when the principal can both update the flow destination AND start the flow immediately (matched(optional[0])). CONDITIONAL(trigger_exists) when the principal can update the flow but NOT start it immediately — the exfil occurs on the next scheduled run or event trigger (the flow still fires). CONDITIONAL(resource_policy) in all cases unless the attacker also controls the destination bucket policy (to grant appflow.amazonaws.com write access) — without that grant, the redirected flow run fails at the S3 write step. Never ACTIVE when the destination bucket policy gating is unresolved. Inherit weakest state from CanModifyConfiguration and optional CanWriteData. |
Narrative
{principal.name} can update {flow.name}'s destination (appflow:UpdateFlow) to an attacker-controlled S3 bucket; when the flow next runs (immediately via appflow:StartFlow, or on its next scheduled/event trigger), AppFlow copies the SaaS data it reads ({flow.source_connector_type}: {flow.source_object}) to the attacker's bucket, exfiltrating it outside the account.
Raw rule rules/derived/aws/appflow.yaml
id: aws-appflow-update-flow-exfil
emits: CanExfiltrate
description: Redirecting an existing flow's destination to an attacker-controlled endpoint and triggering
the flow exfiltrates the SaaS data it reads (Salesforce records, Zendesk tickets, Slack messages, etc.).
applies_to:
- aws
match:
- - principal: null
- CanModifyConfiguration
- flow: null
where:
- node_type(?flow) == GenericMessaging or node_class(?flow) == Messaging
- ?flow is an AWS::AppFlow::Flow
- "?flow has a configured source (SaaS connector profile) with data \u2014 i.e. the flow reads non-empty\
\ data"
optional:
- - principal: null
- CanWriteData
- flow: null
emit:
source_type: Identity
target_type: Data
source: ?principal
target: <data read by ?flow from its SaaS source (e.g. Salesforce objects, Zendesk tickets)>
permissions:
- appflow:UpdateFlow
- appflow:StartFlow?
conditions:
- iam_permission
- resource_policy
- trigger_exists
state_logic: "ACTIVE when the principal can both update the flow destination AND start the flow immediately\
\ (matched(optional[0])). CONDITIONAL(trigger_exists) when the principal can update the flow but NOT\
\ start it immediately \u2014 the exfil occurs on the next scheduled run or event trigger (the flow\
\ still fires). CONDITIONAL(resource_policy) in all cases unless the attacker also controls the destination\
\ bucket policy (to grant appflow.amazonaws.com write access) \u2014 without that grant, the redirected\
\ flow run fails at the S3 write step. Never ACTIVE when the destination bucket policy gating is unresolved.\
\ Inherit weakest state from CanModifyConfiguration and optional CanWriteData."
confidence: min(contributing_confidences) * 0.85
derived_from:
- ?principal CanModifyConfiguration ?flow
- '?principal CanWriteData ?flow (optional: appflow:StartFlow)'
- 'messaging-chains.yaml (cited: data-plane exfil from messaging config modification)'
false_positive_note: "TWO GATING CONDITIONS must both be met: (a) the principal holds appflow:UpdateFlow\
\ on the flow ARN, and (b) the attacker-controlled destination S3 bucket already has an appflow.amazonaws.com\
\ write grant in its bucket policy (or the attacker can add one via s3:PutBucketPolicy). Without (b),\
\ UpdateFlow succeeds in config terms but the flow run fails at the S3 write. Emit as CONDITIONAL(resource_policy)\
\ when (b) is not confirmed. Also: a flow whose source connector profile has no data (empty SaaS tenant,\
\ inactive connector) yields no exfiltration impact \u2014 prefer not emitting rather than emitting\
\ a low-value edge. Distinguish from CanWriteData(p, flow) which only means the principal can start\
\ the flow, not redirect it."
narrative: '{principal.name} can update {flow.name}''s destination (appflow:UpdateFlow) to an attacker-controlled
S3 bucket; when the flow next runs (immediately via appflow:StartFlow, or on its next scheduled/event
trigger), AppFlow copies the SaaS data it reads ({flow.source_connector_type}: {flow.source_object})
to the attacker''s bucket, exfiltrating it outside the account.'