Harness-engineering owasp-idor-prevention

OWASP IDOR Prevention

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/owasp-idor-prevention" ~/.claude/skills/intense-visions-harness-engineering-owasp-idor-prevention-38ddf9 && rm -rf "$T"
manifest: agents/skills/codex/owasp-idor-prevention/SKILL.md
source content

OWASP IDOR Prevention

Enforce object-level authorization so users can only access resources they own or are permitted to access

When to Use

  • Building any endpoint that fetches a resource by ID (GET /orders/:id, GET /files/:id)
  • Implementing multi-tenant applications where data isolation is critical
  • Reviewing CRUD APIs for missing ownership checks
  • Designing authorization middleware or guards for resource access
  • Building admin vs. user role separation

Instructions

Always Scope Queries to the Authenticated User

The most reliable pattern: include

userId
in every database query, not just in authorization checks after the fact.

// BAD — fetches by ID only, attacker can enumerate any order
app.get('/orders/:id', authenticate, async (req, res) => {
  const order = await db.order.findUnique({ where: { id: req.params.id } });
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order); // returns order even if it belongs to another user
});

// GOOD — scope the query to the current user
app.get('/orders/:id', authenticate, async (req, res) => {
  const order = await db.order.findFirst({
    where: {
      id: req.params.id,
      userId: req.user.id, // IDOR prevention at query level
    },
  });
  if (!order) return res.status(404).json({ error: 'Not found' }); // same error for not found and unauthorized
  res.json(order);
});

Authorization Service Pattern

Centralize ownership checks in a dedicated service to avoid scattered, inconsistent checks:

// authorization.service.ts
@Injectable()
export class AuthorizationService {
  constructor(private db: PrismaService) {}

  async assertOwnsOrder(userId: string, orderId: string): Promise<Order> {
    const order = await this.db.order.findFirst({
      where: { id: orderId, userId },
    });
    if (!order) throw new ForbiddenException('Access denied');
    return order;
  }

  async assertCanAccessDocument(userId: string, docId: string): Promise<Document> {
    const doc = await this.db.document.findFirst({
      where: {
        id: docId,
        OR: [
          { ownerId: userId },
          { sharedWith: { some: { userId } } },
        ],
      },
    });
    if (!doc) throw new ForbiddenException('Access denied');
    return doc;
  }
}

// order.controller.ts
@Get(':id')
async getOrder(@Param('id') id: string, @CurrentUser() user: User) {
  return this.authz.assertOwnsOrder(user.id, id);
}

Role-Based Access with Resource Guards

// resource-owner.guard.ts
@Injectable()
export class ResourceOwnerGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private authz: AuthorizationService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const userId = req.user.id;
    const resourceId = req.params.id;
    const resourceType = this.reflector.get<string>('resourceType', context.getHandler());

    await this.authz.assertOwns(userId, resourceType, resourceId);
    return true;
  }
}

// Usage
@Get(':id')
@SetMetadata('resourceType', 'order')
@UseGuards(ResourceOwnerGuard)
getOrder(@Param('id') id: string) { ... }

Indirect Reference Maps (UUIDs over Sequential IDs)

Use UUIDs for public-facing resource identifiers. Sequential IDs make enumeration trivial.

// PREDICTABLE — easy to enumerate
// GET /invoices/1, /invoices/2, /invoices/3 ...

// BETTER — UUID makes guessing computationally infeasible
// GET /invoices/550e8400-e29b-41d4-a716-446655440000

// Prisma schema
model Order {
  id     String @id @default(uuid())
  userId String
  // ...
}

For extra security with internal sequential IDs, map them to tokens:

// Return opaque tokens instead of raw IDs
function encodeResourceId(internalId: number, secret: string): string {
  // Use a deterministic encryption (not hash) so you can decode
  return Buffer.from(`${internalId}:${secret}`).toString('base64url');
}

Details

IDOR (also called BOLA — Broken Object Level Authorization) is the #1 API security issue per OWASP API Security Top 10. Authentication tells you WHO the user is; authorization tells you WHAT they can access.

Common IDOR mistakes:

  • Checking auth at controller level but using unchecked ID in a service call
  • Returning 403 only after fetching the resource (information still leaked in timing)
  • Trusting user-supplied
    userId
    in request body instead of the JWT/session

Return 404 not 403 for unauthorized resources — returning 403 confirms the resource exists, enabling enumeration.

Audit checklist:

  • Every
    findById
    /
    findUnique
    call — does it include
    userId
    or equivalent scope?
  • Every bulk list endpoint — does it filter to current user's data?
  • Admin routes — are they properly gated behind role checks?

Source

https://owasp.org/www-project-top-ten/

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.