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.
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.
Use human-readable slugs for tenant-identifying URL segments, enforced at the schema level:
// 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.
ID: saas-multi-tenancy.tenant-management.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:
// 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.