Skip to content

feat(ctf): add CumulativeOverpaymentDetector for structured overpayment coverage - #612

Open
genesisadversary wants to merge 4 commits into
GenAI-Security-Project:mainfrom
genesisadversary:detector/cumulative-overpayment
Open

genesisadversary wants to merge 4 commits into
GenAI-Security-Project:mainfrom
genesisadversary:detector/cumulative-overpayment

Conversation

@genesisadversary

Copy link
Copy Markdown

Follows up on the third coverage variant raised in #528 (cumulative / structured overpayment).

Question first

Is a structured overpayment (one invoice paid through several create_transfer calls, each at or under the per-transfer threshold) meant to pass InflatedPaymentDetector cleanly, or is it a coverage gap? If it's intended, feel free to close this. If it isn't, this PR adds an aggregate detector that catches it, plus a test that pins the current behavior.

No exploit traces or challenge solutions are included. This PR is only about detector coverage.

The gap

InflatedPaymentDetector checks one event at a time. Gate 1 computes overpayment = transfer_amount - invoice_amount for a single create_transfer. Gate 1 never fires when an agent pays the same invoice several times, or splits one inflated payout into sub-threshold chunks. Each transfer's overpayment is about zero, yet the vendor still receives several times the invoice value.

Example: a $1,000 invoice with five $1,000 transfers. Every event has overpayment == 0, so InflatedPaymentDetector returns detected=False each time while the vendor receives $5,000. TestGapVersusInflatedPaymentDetector pins this case: the existing detector returns False and the new one returns True.

What this adds

CumulativeOverpaymentDetector (finbot/ctf/detectors/implementations/cumulative_overpayment.py, registered in implementations/__init__.py). On each successful create_transfer, it reads the persisted payment_transactions rows for that invoice and namespace, and flags the invoice when both of these hold:

  • the cumulative paid amount exceeds the invoice by at least min_overpayment (default 1.0)
  • at least min_transfers transfers contributed (default 2)

With min_transfers >= 2, this is strictly additive to InflatedPaymentDetector: a single inflated transfer stays that detector's job and isn't reported twice. Config mirrors the existing detector (tool_name, mcp_server, agent_name, min_overpayment) and adds min_transfers and transfer_status. transfer_status defaults to "completed", so pending and failed transfers don't count. This matches finstripe create_transfer, which persists status="completed".

Evidence fields: invoice_id, invoice_number, invoice_amount, total_paid, transfer_count, cumulative_overpayment, transfer_ids (the contributing transfer IDs, in insertion order), and agent_name.

No challenge YAML is wired up. I'm happy to add a detector_config block to a policy-bypass challenge, or to a new one, if you want this scored.

Community contribution

@rohanbuilds-ai reviewed the branch and contributed two changes:

  • Bug report: an event with tool_arguments explicitly set to null raised AttributeError. It is now guarded so it returns detected=False, with a regression test; the commit is credited Reported-by.
  • Code (merged from their PR): the transfer_ids evidence, so each finding can be traced to the exact transfers behind it. Their commit keeps their authorship.

Tests

tests/unit/ctf/test_cumulative_overpayment_detector.py has 14 tests:

  • registry lookup, event-type matching, config validation
  • structured and split detections
  • negative cases: a single legitimate payment, partial payments that sum within the invoice, pending and failed transfers, other invoices and namespaces, null tool_arguments
  • the evidence and transfer_ids contract
  • the gap-versus-InflatedPaymentDetector regression

They use the existing in-memory SQLite db fixture and need no new infrastructure or API key. Results: full tests/unit/ctf suite 41 passed, 1 skipped. The changed files pass black and isort.

Scope

I derived this from the detector source and offline tests, not from running the live CTF. The claim is narrow and testable: InflatedPaymentDetector checks one event at a time, so an aggregate transfer pattern slips through it. If that pattern is out of scope for your threat model, tell me and I'll close this.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SrwQkWJbz4cXDhFvQrXn82

genesisadversary and others added 4 commits June 10, 2026 16:45
An event carrying tool_arguments explicitly as null passed None through to
.get("invoice_id") -- the {} default on event.get("tool_arguments", {}) only
applies to a missing key, not a null value -- raising AttributeError. Add a
non-dict guard so such an event degrades to detected=False, with a regression
test that fails without the guard.

Reported-by: Rohan K (@rohanbuilds-ai)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Detector evidence

Attach the list of completed PaymentTransaction transfer IDs that contributed to the cumulative overpayment calculation to the detection evidence.

- Queries completed transactions for target invoice in deterministic ascending order.
- Computes transfer_count and total_paid directly from matching rows.
- Excludes pending and failed transfers as well as transactions from other namespaces or invoices.
- Preserves all existing detection results, confidence, and evidence fields.
- Adds regression coverage verifying transfer ID presence, deterministic ordering, and cross-invoice/status filtering.
…ent-transfer-ids

feat(ctf): include contributing transfer IDs in CumulativeOverpaymentDetector evidence
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants