plugins/security-guardian/skills/owasp/access-control/SKILL.md
OWASP A05 - Broken Access Control Detection. Use this skill when implementing authorization, checking permissions, or auditing who can access what resources. Activate when: authorization, permissions, access control, RBAC, ABAC, admin access, privilege escalation, IDOR, direct object reference, role check, can user access.
npx skillsauth add latestaiagents/agent-skills access-control-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Detect and fix broken access control vulnerabilities including IDOR, privilege escalation, and missing authorization checks.
| Vulnerability | Risk | Example |
|--------------|------|---------|
| IDOR | HIGH | /api/users/123 accessible by any user |
| Missing Auth Check | CRITICAL | Admin endpoints without verification |
| Privilege Escalation | CRITICAL | User can elevate to admin |
| Path Traversal | HIGH | ../../admin/config |
| Forced Browsing | MEDIUM | Guessing admin URLs |
| Metadata Manipulation | HIGH | Changing userId in JWT |
// VULNERABLE - No ownership check
app.get('/api/documents/:id', async (req, res) => {
const doc = await Document.findById(req.params.id);
res.json(doc); // Anyone can access any document!
});
// VULNERABLE - No role check on admin route
app.delete('/api/users/:id', async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.json({ success: true }); // Any user can delete anyone!
});
// VULNERABLE - Client-side only checks
// Frontend hides admin button, but API has no check
if (user.role === 'admin') {
showAdminButton();
}
// VULNERABLE - Sequential IDs exposed
app.get('/api/invoices/:id', (req, res) => {
// User can increment ID to see other invoices
const invoice = await Invoice.findById(req.params.id);
res.json(invoice);
});
// VULNERABLE - User ID from request body
app.post('/api/profile/update', (req, res) => {
// Attacker can change userId to modify others
await User.findByIdAndUpdate(req.body.userId, req.body.data);
});
// VULNERABLE - Role from user input
app.post('/api/users', (req, res) => {
const user = new User({
email: req.body.email,
role: req.body.role // Attacker sets role: 'admin'
});
});
// VULNERABLE - Mass assignment
app.put('/api/profile', (req, res) => {
// Attacker adds { isAdmin: true } to request
await User.findByIdAndUpdate(userId, req.body);
});
// Authorization middleware factory
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!allowedRoles.includes(req.user.role)) {
// Log authorization failure
logger.warn('Authorization failed', {
userId: req.user.id,
requiredRoles: allowedRoles,
userRole: req.user.role,
path: req.path
});
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.delete('/api/users/:id',
authenticate,
authorize('admin'),
deleteUserHandler
);
app.get('/api/reports',
authenticate,
authorize('admin', 'manager'),
getReportsHandler
);
// Ownership check middleware
function verifyOwnership(resourceModel, paramName = 'id') {
return async (req, res, next) => {
const resourceId = req.params[paramName];
const resource = await resourceModel.findById(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Check ownership (adjust field name as needed)
const isOwner = resource.userId?.toString() === req.user.id;
const isAdmin = req.user.role === 'admin';
if (!isOwner && !isAdmin) {
logger.warn('Ownership check failed', {
userId: req.user.id,
resourceId,
resourceOwner: resource.userId
});
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
};
}
// Usage
app.get('/api/documents/:id',
authenticate,
verifyOwnership(Document),
(req, res) => res.json(req.resource)
);
app.put('/api/documents/:id',
authenticate,
verifyOwnership(Document),
updateDocumentHandler
);
const { Ability, AbilityBuilder } = require('@casl/ability');
function defineAbilitiesFor(user) {
const { can, cannot, build } = new AbilityBuilder(Ability);
if (user.role === 'admin') {
can('manage', 'all'); // Admin can do everything
} else if (user.role === 'manager') {
can('read', 'all');
can('update', 'Document', { departmentId: user.departmentId });
can('create', 'Document');
cannot('delete', 'Document');
} else {
// Regular user
can('read', 'Document', { userId: user.id });
can('update', 'Document', { userId: user.id });
can('create', 'Document');
can('read', 'Profile', { userId: user.id });
can('update', 'Profile', { userId: user.id });
}
return build();
}
// Middleware
function checkAbility(action, subject) {
return (req, res, next) => {
const ability = defineAbilitiesFor(req.user);
if (ability.can(action, subject)) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
};
}
// Usage
app.delete('/api/documents/:id',
authenticate,
checkAbility('delete', 'Document'),
deleteHandler
);
// Whitelist allowed fields
const ALLOWED_PROFILE_FIELDS = ['name', 'email', 'avatar', 'bio'];
const ALLOWED_ADMIN_FIELDS = [...ALLOWED_PROFILE_FIELDS, 'role', 'isActive'];
function filterFields(data, allowedFields) {
return Object.keys(data)
.filter(key => allowedFields.includes(key))
.reduce((obj, key) => {
obj[key] = data[key];
return obj;
}, {});
}
app.put('/api/profile', authenticate, async (req, res) => {
const allowedFields = req.user.role === 'admin'
? ALLOWED_ADMIN_FIELDS
: ALLOWED_PROFILE_FIELDS;
const safeData = filterFields(req.body, allowedFields);
await User.findByIdAndUpdate(req.user.id, safeData);
res.json({ success: true });
});
const { v4: uuidv4 } = require('uuid');
// Mongoose schema with UUID
const documentSchema = new mongoose.Schema({
_id: {
type: String,
default: uuidv4
},
title: String,
userId: String
});
// Or use mongoose-uuid
const mongoose = require('mongoose');
require('mongoose-uuid')(mongoose);
const accessMatrix = {
'GET /api/users': ['admin'],
'GET /api/users/:id': ['admin', 'self'],
'PUT /api/users/:id': ['admin', 'self'],
'DELETE /api/users/:id': ['admin'],
'GET /api/documents': ['admin', 'user'],
'GET /api/documents/:id': ['admin', 'owner'],
'POST /api/documents': ['admin', 'user'],
'PUT /api/documents/:id': ['admin', 'owner'],
'DELETE /api/documents/:id': ['admin', 'owner'],
'GET /api/admin/*': ['admin'],
'POST /api/admin/*': ['admin']
};
function checkAccess(req, resourceOwnerId = null) {
const route = `${req.method} ${req.route.path}`;
const allowedRoles = accessMatrix[route] || [];
if (allowedRoles.includes(req.user.role)) {
return true;
}
if (allowedRoles.includes('self') && req.params.id === req.user.id) {
return true;
}
if (allowedRoles.includes('owner') && resourceOwnerId === req.user.id) {
return true;
}
return false;
}
# Test IDOR - Try accessing other users' resources
curl -H "Authorization: Bearer $USER_A_TOKEN" \
http://api.com/users/USER_B_ID
# Test privilege escalation
curl -X PUT http://api.com/profile \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"role": "admin"}'
# Test missing auth
curl http://api.com/admin/users
# Test forced browsing
curl http://api.com/admin
curl http://api.com/backup
curl http://api.com/config
# Automated testing
# Use Burp Suite's Authorize extension
# OWASP ZAP's Access Control Testing
development
Test skills for correct activation, content quality, and regression — both automated checks (frontmatter validity, lint) and manual verification (query-suite activation testing). Covers CI integration and how to catch skill regressions before users do. Use this skill when adding skills to a repo, setting up CI for a skill library, or debugging "the skill exists but doesn't work". Activate when: test skills, validate skills, skill CI, skill linting, skill activation test, skill regression.
documentation
Write the YAML frontmatter for a SKILL.md file so it activates reliably — name, description, and activation keywords that the model matches against. Covers length, tone, and the most common frontmatter mistakes. Use this skill when authoring a new skill, fixing a skill that isn't auto-activating, or reviewing skills for publication. Activate when: SKILL.md frontmatter, skill description, skill activation, skill YAML, write a skill, author a skill.
development
Design skills that fire at the right moment — neither over-eager (noise) nor under-eager (silent). Covers activation specificity, trigger phrases, disambiguation between overlapping skills, and debugging activation. Use this skill when multiple skills could fire on the same query, a skill never fires, or a skill fires too often. Activate when: skill won't activate, skill over-activates, overlapping skills, skill triggers, skill selection, skill disambiguation.
development
Structure SKILL.md content so the model reads just enough — concise summary up front, progressively deeper detail, examples on demand. Covers section ordering, length budgets, when to split into multiple skills. Use this skill when writing or refactoring a skill body, one skill has grown too long, or a skill is wordy but not useful. Activate when: SKILL.md structure, skill content, skill too long, split skill, progressive disclosure, skill body.