Skip to content

fix(cors): per-merchant CORS origin validation and DELETE endpoint#32

Open
Jaydbrown wants to merge 4 commits into
OrbitStream:mainfrom
Jaydbrown:fix/issue-19-cors-origin-validation
Open

fix(cors): per-merchant CORS origin validation and DELETE endpoint#32
Jaydbrown wants to merge 4 commits into
OrbitStream:mainfrom
Jaydbrown:fix/issue-19-cors-origin-validation

Conversation

@Jaydbrown

Copy link
Copy Markdown

Closes #19

Summary

The SetCorsOriginsDto lacked the security constraints described in the issue, and there was no way to remove a single origin without replacing the entire list. This PR adds all missing validation rules and the DELETE /merchants/me/cors/:origin endpoint.

Changes

src/merchants/merchants.dto.ts

Before:

export class SetCorsOriginsDto {
  @IsArray()
  @IsUrl({}, { each: true })
  origins: string[];
}

After:

export class SetCorsOriginsDto {
  @IsArray()
  @ArrayMaxSize(10, { message: 'A maximum of 10 CORS origins are allowed per merchant' })
  @IsUrl(
    { protocols: ['https'], require_protocol: true, allow_localhost: false },
    { each: true, message: 'Each origin must be a valid HTTPS URL without localhost' },
  )
  @Matches(/^[^*]+$/, { each: true, message: 'Wildcard origins are not permitted' })
  origins: string[];
}

Validation added:

  • @ArrayMaxSize(10) — enforces the 10-origin cap at the DTO layer before any DB write
  • protocols: ['https'] — rejects HTTP origins
  • require_protocol: true — prevents bare hostnames like example.com
  • allow_localhost: false — blocks localhost in all environments
  • @Matches(/^[^*]+$/) — explicitly rejects wildcard strings

src/merchants/merchants.service.ts

Added deleteCorsOrigin(merchantId, origin):

async deleteCorsOrigin(merchantId: string, origin: string): Promise<boolean> {
  const current = await this.getCorsOrigins(merchantId);
  const filtered = current.filter((o: string) => o !== origin);
  if (filtered.length === current.length) return false;
  await this.setCorsOrigins(merchantId, filtered);
  return true;
}

Delegates to setCorsOrigins so the Redis cache is invalidated via the existing corsCache.invalidateMerchantCache(merchantId) call — no separate cache logic needed.

src/merchants/merchants.controller.ts

Added DELETE /merchants/me/cors/:origin:

@Delete('me/cors/:origin')
@Roles('admin', 'merchant')
async deleteCorsOrigin(@Request() req: any, @Param('origin') origin: string) {
  const m = await this.merchants.findByWallet(req.user.walletAddress);
  if (!m) throw new NotFoundException('Merchant not found');
  const removed = await this.merchants.deleteCorsOrigin(m.id, decodeURIComponent(origin));
  if (!removed) throw new NotFoundException('Origin not found in configured list');
  return { origins: await this.merchants.getCorsOrigins(m.id) };
}

decodeURIComponent handles URL-encoded origins in the path (e.g. https%3A%2F%2Fexample.com). Returns the updated list so the caller does not need a follow-up GET.

Validation matrix

Input Result
https://myshop.com ✅ accepted
http://myshop.com ❌ rejected (HTTP)
myshop.com ❌ rejected (no protocol)
https://localhost:3000 ❌ rejected (localhost)
https://*.myshop.com ❌ rejected (wildcard)
11 origins in one request ❌ rejected (exceeds max 10)

Test plan

  • PUT /merchants/me/cors with an HTTP origin → 400
  • PUT /merchants/me/cors with a localhost origin → 400
  • PUT /merchants/me/cors with a wildcard origin → 400
  • PUT /merchants/me/cors with 11 origins → 400
  • PUT /merchants/me/cors with valid HTTPS origins → 200, saved and cached
  • DELETE /merchants/me/cors/:origin with an existing origin → 200, returns updated list
  • DELETE /merchants/me/cors/:origin with a non-existent origin → 404
  • Verify Redis cache is invalidated after both PUT and DELETE

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.

[FEAT] Per-Merchant CORS Origin Configuration

2 participants