# URL patterns do not expose internal tenant IDs

- **Pattern:** `ab-002354` (`saas-multi-tenancy.tenant-management.url-no-internal-ids`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002354
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002354)

## Why it matters

Sequential integer organization IDs in URLs (e.g., `/orgs/1042/`) allow any authenticated user to enumerate all tenants by incrementing the ID — a reconnaissance step that maps your customer base, exposes competitor relationships, and simplifies targeted IDOR attacks. CWE-200 and CWE-639 both apply. While this is lower severity than direct data exposure, it is a forcing function for more serious attacks and may violate contractual confidentiality obligations between competing tenants on the same platform.

## Severity rationale

Low because ID exposure alone does not grant data access, but it significantly reduces the effort required to mount enumeration or IDOR attacks against other tenants.

## Remediation

Use human-readable slugs for tenant-identifying URL segments, enforced at the schema level:

```prisma
// schema.prisma
model Organization {
  id   String @id @default(uuid())
  slug String @unique  // e.g., "acme-corp" — enforced unique by DB index
}
```

Generate the slug from the organization name on creation using a library like `slugify`, and resolve collisions with a numeric suffix (`acme-corp-2`). Update all route definitions from `/orgs/[id]/` to `/orgs/[slug]/` in `src/app/`. If UUIDs must be used (e.g., for API compatibility), ensure they are v4 (random) — not v1 (timestamp-based) or sequential, which are enumerable.

## Detection

- **ID:** `url-no-internal-ids`
- **Severity:** `low`
- **What to look for:** Examine URL patterns used in the application — route definitions, navigation links, API endpoint paths. Check whether internal database identifiers (UUID primary keys, auto-increment IDs) for tenants appear directly in user-facing URLs. Compare against whether a slug, subdomain, or opaque identifier is used instead. Internal IDs in URLs are a minor privacy/reconnaissance concern and can simplify enumeration attacks.
- **Pass criteria:** List all URL-generation functions. Tenant-identifying segments in at least 100% of URLs use human-readable slugs (e.g., `/orgs/acme-corp/`) or subdomains (`acme.app.com`) rather than raw database UUIDs or sequential integers. If UUIDs are used, they are at minimum v4 (random, not enumerable).
- **Fail criteria:** User-facing URLs contain sequential integer IDs for tenants (e.g., `/orgs/1234/`) that allow enumeration. Internal database UUIDs are exposed in URLs unnecessarily when a slug would work.
- **Skip (N/A) when:** No URL-based routing for tenant resources is detected, or all tenant routing is via subdomain with no path-based tenant identifier.
- **Detail on fail:** Example: `"Routes use /orgs/{integer_id}/ pattern. Organization IDs are sequential integers starting from 1, allowing enumeration of all organizations by incrementing the ID."`
- **Remediation:** Use slugs for tenant-identifying URL segments:

  ```ts
  // Schema: add a unique slug to Organization
  model Organization {
    id   String @id @default(uuid())
    slug String @unique  // e.g., "acme-corp"
  }

  // Routing uses the slug, not the ID
  // /orgs/acme-corp/settings instead of /orgs/550e8400-e29b-.../settings
  ```

  Generate slugs from the organization name on creation (using a library like `slugify`) and handle collisions with a numeric suffix.

---

## External references

- cwe CWE-639
- cwe CWE-200
- owasp A01

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002354
