fix(sns__enum): paginate list_topics and list_subscriptions_by_topic to prevent enumeration false-negatives - #534
Conversation
sns__enum called list_topics() and list_subscriptions_by_topic() exactly once per region/topic, ignoring the NextToken field. SNS paginates both APIs at 100 items per page, so accounts with >100 topics (or topics with >100 subscriptions) were silently under-reported — an enumeration false-negative in a red-team tool. Both calls now follow NextToken until exhausted. Adds four moto-backed regression tests under the module's tests/ subdir: the two pagination tests fail against the pre-fix code and pass against the fix. Signed-off-by: Devam Shah <devamshah91@gmail.com>
…idiom A bare `while "NextToken" in response` loop does not terminate when the final page carries `NextToken: ""` instead of omitting the key. That is the same defect class as RhinoSecurityLabs#503 (secrets__enum infinite loop from broken pagination), and an infinite loop in an enum module is strictly worse than the truncation this PR set out to fix. Both loops now guard with `and response["NextToken"] != ""`, which is the existing idiom in glue__enum/main.py:56 and transfer_family__enum/main.py:44, and is equivalent to the `if not next_token: break` form RhinoSecurityLabs#503 landed in secrets__enum. Adds test_empty_next_token_terminates_pagination: a fake SNS client whose last page returns an empty NextToken, with a hard call cap so a regression fails fast instead of hanging the suite. It fails (26 list_topics calls, expected 2) against the unguarded loop. Also drops the unused `import pytest` (flake8 F401) from the test module. Signed-off-by: Devam Shah <devamshah91@gmail.com>
|
@nobodynate — pushed Loop guard: an empty
|
| call site | API |
|---|---|
vpc__enum_lateral_movement/main.py:74 |
describe_direct_connect_gateways() — unfiltered |
vpc__enum_lateral_movement/main.py:81 |
describe_direct_connect_gateway_associations(directConnectGatewayId=…) |
vpc__enum_lateral_movement/main.py:142 |
describe_vpc_peering_connections() — unfiltered |
elasticbeanstalk__enum/main.py:131 |
describe_environments(ApplicationName=…) |
elasticbeanstalk__enum/main.py:149 |
describe_environments() — unfiltered |
All five come back can_paginate=True. I have not measured the server-side page size for any of them, so I can't tell you how many resources it takes to trip each one — but the failure mode is identical to the SNS one: a clean-looking result that silently omits everything past page one, and vpc__enum_lateral_movement in particular feeds pivot analysis. I have not touched any of them. Happy to fix them in this PR or as a separate one, whichever is easier for you to review.
Things I checked and left alone: describe_applications(), describe_vpn_connections(), and describe_vpn_gateways() report can_paginate=False, and describe_application_versions at elasticbeanstalk__enum/main.py:230 is filtered to a single VersionLabels entry, so none of those truncate in practice.
State
The branch is still on top of current master (e597f23) — nothing to rebase. GitHub reports MERGEABLE / BLOCKED with no checks on the branch, which I read as waiting on a maintainer approval rather than anything on my end, but tell me if there's a check I should be triggering.
Would appreciate a review when you have a cycle.
Summary
sns__enumcalledlist_topics()andlist_subscriptions_by_topic()exactlyonce per region/topic, silently capping enumeration at 100 items per call and
producing false-negatives in accounts with more than 100 SNS topics or more than
100 subscriptions on a single topic.
Problem / Motivation
The AWS SNS API paginates
list_topicsat 100 topics per response andlist_subscriptions_by_topicat 100 subscriptions per response. Prior to thisfix, both calls were made exactly once — the
NextTokenfield in the responsewas ignored. In any AWS account where either threshold is crossed,
sns__enumwould silently under-report:
their subscribers — would never be examined.
100 subscribers.
This is a direct enumeration false-negative: an operator running Pacu in a
large-scale AWS environment receives a clean-looking report that materially
misrepresents the SNS attack surface. From a threat-modelling perspective the
missed topics could include SNS→Lambda chains, cross-account notification
channels, or data-exfiltration primitives that a red-team or a defender needs to
account for (MITRE ATT&CK T1526 — Cloud Service Discovery).
The pattern is identical to the bug fixed in PR #503 and mirrors pagination
already correctly implemented in
codebuild__enum(nextToken) anddynamodb__enum(LastEvaluatedTableName).Change
pacu/modules/sns__enum/main.py:client.list_topics()call with aNextTokenwhile-loopthat accumulates all topic ARNs into
all_topicsbefore iterating.client.list_subscriptions_by_topic()call per topic witha
NextTokenwhile-loop that accumulates all subscriptions intoall_subscriptionsbefore appending to the result.and response["NextToken"] != "". A barewhile "NextToken" in responsedoes not terminate when the terminal pagecarries an empty token instead of omitting the key — the same defect class as
fix: secrets__enum infinite loop from broken pagination (closes #487) #503. This matches the idiom in
glue__enum/main.py:56andtransfer_family__enum/main.py:44.the fix is narrowly scoped to the two pagination gaps.
pacu/modules/sns__enum/tests/test_sns__enum.py(new file):moto.mock_aws:test_list_topics_follows_next_token— creates 105 topics, asserts all 105are in the result (fails on the unfixed code).
test_list_subscriptions_by_topic_follows_next_token— creates 1 topic with105 subscriptions, asserts all 105 subscribers are collected (fails on the
unfixed code).
test_single_page_topics_unaffected— smoke test confirming the common(<100 topics) path is unchanged.
test_empty_region_excluded_from_result— confirms the existing behaviourof dropping empty regions is preserved.
test_empty_next_token_terminates_pagination— a fake client whose lastpage returns
NextToken: "", with a hard call cap so a regression failsfast instead of hanging the suite.
The tests live in the module's
tests/subdir, matching the existing conventionused by
cfn__resource_injectionandcognito__attack.pyproject.toml'stestpathsalready includespacu/modules, sopytest/python -m pytestdiscovers them automatically. Note: the
Makefiletesttarget currently onlypoints at
./testsand the cfn lambda tests, somake testwill not pick theseup as-is — if maintainers prefer, the
testtarget can be widened, but that isout of scope here.
Security Rationale
Incomplete cloud asset enumeration is a well-documented source of security
blind-spots. An assessor who misses 50 SNS topics in a large AWS account may
overlook notification-triggered Lambda functions, cross-account delivery
channels, or subscriber endpoints that are in scope for privilege-escalation or
data-exfiltration analysis. Pacu is a red-team tool; its enumeration modules are
the foundation on which every subsequent attack module depends. A false-negative
here propagates silently downstream.
This class of bug — assuming a single paginated API response is exhaustive —
maps to incomplete enumeration under MITRE ATT&CK T1526 and is the exact defect
pattern documented in CWE-390 (Detection of Error Condition Without Action) when
the
NextTokenpresence is passively ignored.Testing / Validation
The three pagination tests fail deterministically against the pre-fix code and
pass deterministically against the fix. The two behaviour-preservation tests
(single page, empty region) pass in both directions.
Note: the
Makefiletesttarget CI runs points only at./testsand the cfnlambda tests (53 of the 61 above), so CI will not execute these tests as-is.
Switching it to
python3 -m pytestpicks up all 61 and passes clean. Leftunchanged here as a maintainer call.