F-2026-0002·broken-object-level-authorization

Potential IDOR: Cross-Partner API Key Management

Acknowledgedpentestbackendapigithub.com/bloom-art/api
TL;DR

The Partner Admin controller's API key lifecycle endpoints take `partnerId` directly from the URL with no ownership check, allowing any `partnerAdmin` key holder to manage API keys for any partner. Accepted as by design because `partnerAdmin` is currently a Dripster-internal operator role.

Severity
MEDIUM
Impact
HIGH
Likelihood
LOW
Method
MManual review
CAT.
Complexity
LOW
Exploitability
MEDIUM
02Section · Description

Description

The Partner Admin controller uses AdminOrPartnerAdminApiKeyGuard for API key lifecycle operations (create, list, delete). This guard allows both admin and partnerAdmin roles. However, the partnerId used in the operation is taken directly from the URL path parameter, there is no validation that the authenticated partnerAdmin's API key actually belongs to the target partner specified in the URL.

typescript
// partner-admin.controller.ts L74-L88
@UseGuards(AdminOrPartnerAdminApiKeyGuard) // allows ANY partnerAdmin
@HttpCode(HttpStatus.CREATED)
@Post(buildAdminPartnersApiKeysPath())
public async createApiKey(
@Param(apiPathParam.partnerId) partnerId: string, // from URL, no ownership check
@Body(new CamelizeBodyPipe()) body: CreatePartnerApiKeyBody,
): Promise<Decamelized<CreatePartnerApiKeyResult>> {
try {
const result = await this.partnerService.createApiKeyForPartner({ partnerId, role: body.role });
// partnerId from URL is passed directly, no check against caller's partnerId
return this.apiWrapperService.formatResponse(result);
} catch (error) {
return rethrowProperErrorForRestWithNotFound(error, [PartnerNotFoundError], [PartnerApiKeyLimitExceededError]);
}
}

The service layer only checks that the target partner exists, not that the caller belongs to it:

typescript
// partner.service.ts L76-L88
public async createApiKeyForPartner(input: CreatePartnerApiKeyInput): Promise<CreatePartnerApiKeyResult> {
const partner = await this.partnerRepository.findByPartnerId(input.partnerId);
if (!partner) {
throw new PartnerNotFoundError(input.partnerId); // only checks existence, NOT ownership
}
const apiKey = await this.apiKeyService.createApiKey({ partnerId: input.partnerId, role: input.role });
return {
id: apiKey.apiKeyId,
publicId: apiKey.publicId,
secretKey: apiKey.plainKey, // plaintext key returned to attacker
createdAt: new Date().toISOString(),
};
}

The identical pattern exists at L90-L103 (listApiKeys) and L105-L118 (deleteApiKey).

Vulnerable scenario:

  1. Partner A has an API key with role partnerAdmin (apiKeyId: ak_A, partnerId: par_A).
  2. Attacker authenticates with Partner A's partnerAdmin key.
  3. Attacker calls POST /admin/partners/par_B/api-keys where par_B is a different partner.
  4. The guard passes because the API key has role partnerAdmin.
  5. PartnerService.createApiKeyForPartner only checks that par_B exists, not that the caller belongs to par_B.
  6. A new API key is created for Partner B and returned to the attacker in plaintext (including the secret key).
  7. Attacker now has API access to Partner B's account.
03Section · Impact

Impact

An attacker with partnerAdmin credentials for one partner might create, list, and delete API keys for every partner on the platform.

04Section · Recommendation

Recommendation

Inject the authenticated user's identity into the controller and validate that the partnerId in the URL matches the partnerId associated with the caller's API key:

typescript
@UseGuards(AdminOrPartnerAdminApiKeyGuard)
@Post(buildAdminPartnersApiKeysPath())
public async createApiKey(
@Param(apiPathParam.partnerId) partnerId: string,
@CurrentApiKey() currentApiKey: LoggedInApiKey,
@Body(new CamelizeBodyPipe()) body: CreatePartnerApiKeyBody,
): Promise<...> {
// Admin keys can manage any partner; partnerAdmin only their own
if (currentApiKey.role === ApiKeyRole.partnerAdmin
&& currentApiKey.partnerId !== partnerId) {
throw new ForbiddenException();
}
// ...
}

Apply the same check to listApiKeys (L90) and deleteApiKey (L105).

05Section · Resolution

Resolution

Acknowledged by team and accepted by auditor as by design. The team clarified that partnerAdmin is a platform-wide internal operator role (only issued to Dripster staff), not a tenant-scoped role assignable to external partners, so cross-partner management is the intended behavior. If partnerAdmin keys are ever issued to external partner companies, an ownership check at AdminOrPartnerAdminApiKeyGuard would become mandatory before that change ships.

F-2026-0002

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx