Skip to content

[PM-36574] fix: scope provider client invoice report to the authorized provider#7770

Merged
connerbw merged 4 commits into
mainfrom
billing/pm-36574/provider-invoice-csv-idor
Jun 22, 2026
Merged

[PM-36574] fix: scope provider client invoice report to the authorized provider#7770
connerbw merged 4 commits into
mainfrom
billing/pm-36574/provider-invoice-csv-idor

Conversation

@connerbw

@connerbw connerbw commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

  • Engineering ticket: PM-36574
  • Source vulnerability: VULN-565 (HackerOne — "Provider Invoice CSV IDOR Across Providers")

📔 Objective

Fixes a broken object-level authorization (IDOR) flaw in the provider client invoice CSV endpoint GET /providers/{providerId}/billing/invoices/{invoiceId}.

The endpoint authorized the caller against the route providerId, but ProviderBillingService.GenerateClientInvoiceReport then loaded ProviderInvoiceItem rows by the caller-supplied invoiceId alone, with no provider-ownership check. A provider admin for provider A who knew provider B's Stripe invoice ID could therefore retrieve provider B's client invoice CSV — client organization name/ID, assigned/used/remaining seats, plan, and estimated total — by requesting B's invoiceId through A's route.

Fix: thread the authorized provider.Id from the controller into GenerateClientInvoiceReport(Guid providerId, string invoiceId), which now reads through a new provider-scoped repository method GetByProviderIdAndInvoiceId. Scoping is enforced at the data-access layer in both ORMs:

  • Dapper — new stored procedure ProviderInvoiceItem_ReadByProviderIdAndInvoiceId (WHERE [ProviderId] = @ProviderId AND [InvoiceId] = @InvoiceId), with SSDT source + migration script.
  • EF Corewhere ProviderId == providerId && InvoiceId == invoiceId.

When the authorized provider doesn't own the requested invoice, the scoped lookup returns nothing and the endpoint now responds 404 Not Found (previously 500) — no cross-provider data is returned and no existence oracle is leaked. The pre-existing unscoped GetByInvoiceId is retained only for the internal Stripe-webhook path (ProviderEventService), which is server-to-server and not attacker-controlled.

Testing: adds an API integration test (ProviderBillingControllerAuthorizationTests) that reproduces the cross-provider attack — failing against the old code (victim CSV returned), passing after the fix (404, no victim data) — plus a control test confirming the attacker cannot use the victim provider's own route. Service/controller unit tests updated for the new signature and 404 behavior.

…d provider

The provider client invoice CSV endpoint authorized the route provider but
generated the report from the caller-supplied invoiceId without verifying the
invoice belonged to that provider. A provider admin could therefore read
another provider's client billing data by passing a foreign invoiceId through
their own provider's route (VULN-565, IDOR / broken object-level authorization).

Thread the authorized provider id from the controller through
GenerateClientInvoiceReport into a new provider-scoped lookup
(GetByProviderIdAndInvoiceId), implemented in both Dapper (stored procedure)
and EF Core, and return 404 when the invoice is not owned by the authorized
provider. The unscoped GetByInvoiceId is retained only for the internal Stripe
webhook path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@connerbw connerbw added the ai-review Request a Claude code review label Jun 5, 2026
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the IDOR fix (VULN-565 / PM-36574) scoping the provider client invoice CSV endpoint to the authorized provider. The fix correctly threads the authorized provider.Id from the controller into GenerateClientInvoiceReport and enforces ownership at the data-access layer in both ORMs. Dapper (new ProviderInvoiceItem_ReadByProviderIdInvoiceId stored procedure with SSDT source + migration) and EF Core (where ProviderId == providerId && InvoiceId == invoiceId) achieve correct parity, and the unscoped GetByInvoiceId is retained only for the server-to-server Stripe webhook path.

The change is well-tested: the new integration test genuinely reproduces the cross-provider attack, and the 404-on-not-owned behavior avoids leaking an existence oracle. No findings.

Code Review Details

No issues found.

Validation notes (not findings):

  • Authorization is correctly enforced at the data layer rather than only the route, closing the IDOR.
  • Dual-ORM parity verified; EF read query needs no migration, SSDT + Migrator script both present and consistently named (matching the resolved naming-convention thread).
  • GetByInvoiceId (unscoped) confirmed used only by ProviderEventService (Stripe webhook, server-to-server).
  • The migration uses CREATE OR ALTER and is additive/idempotent, so its earlier date relative to other pending scripts is not a concern.

@connerbw connerbw marked this pull request as ready for review June 5, 2026 18:26
@connerbw connerbw requested review from a team as code owners June 5, 2026 18:26
@connerbw connerbw requested a review from sbrown-livefront June 5, 2026 18:26
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.52174% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.69%. Comparing base (51192df) to head (15ecb7c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ling/Repositories/ProviderInvoiceItemRepository.cs 0.00% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7770      +/-   ##
==========================================
+ Coverage   61.20%   65.69%   +4.49%     
==========================================
  Files        2209     2209              
  Lines       97732    97750      +18     
  Branches     8813     8816       +3     
==========================================
+ Hits        59812    64216    +4404     
+ Misses      35796    31316    -4480     
- Partials     2124     2218      +94     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

@sbrown-livefront sbrown-livefront left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Great work on this with great test coverage.

public async Task<ICollection<ProviderInvoiceItem>> GetByProviderId(Guid providerId)
{
var sqlConnection = new SqlConnection(ConnectionString);
await using var sqlConnection = new SqlConnection(ConnectionString);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Good catch with these.

@@ -0,0 +1,18 @@
-- Scope the provider client invoice report to the authorized provider (VULN-565 / PM-36574).
-- Adds a provider-scoped lookup so a provider can only read invoice items it owns.
CREATE OR ALTER PROCEDURE [dbo].[ProviderInvoiceItem_ReadByProviderIdAndInvoiceId]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of the stored procedure should be ProviderInvoiceItem_ReadByProviderIdInvoiceId
See docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a commit just now addressing this

…eId per SQL naming convention

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@mkincaid-bw mkincaid-bw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@bre-deploy bre-deploy Bot temporarily deployed to US-QA2 Cloud June 22, 2026 11:26 Inactive
@connerbw connerbw enabled auto-merge (squash) June 22, 2026 16:48
@sonarqubecloud

Copy link
Copy Markdown

@connerbw connerbw merged commit dbf6308 into main Jun 22, 2026
45 of 46 checks passed
@connerbw connerbw deleted the billing/pm-36574/provider-invoice-csv-idor branch June 22, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants