Potential IDOR: Cross-Partner API Key Management
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.
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.
// 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 partnerIdreturn 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:
// partner.service.ts L76-L88public 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 attackercreatedAt: new Date().toISOString(),};}
The identical pattern exists at L90-L103 (listApiKeys) and L105-L118 (deleteApiKey).
Vulnerable scenario:
- Partner A has an API key with role
partnerAdmin(apiKeyId:ak_A, partnerId:par_A). - Attacker authenticates with Partner A's
partnerAdminkey. - Attacker calls
POST /admin/partners/par_B/api-keyswherepar_Bis a different partner. - The guard passes because the API key has role
partnerAdmin. PartnerService.createApiKeyForPartneronly checks thatpar_Bexists, not that the caller belongs topar_B.- A new API key is created for Partner B and returned to the attacker in plaintext (including the secret key).
- Attacker now has API access to Partner B's account.
Impact
An attacker with partnerAdmin credentials for one partner might create, list, and delete API keys for every partner on the platform.
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:
@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 ownif (currentApiKey.role === ApiKeyRole.partnerAdmin&& currentApiKey.partnerId !== partnerId) {throw new ForbiddenException();}// ...}
Apply the same check to listApiKeys (L90) and deleteApiKey (L105).
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.

