Technology Apr 18, 2026 · 9 min read

Broken Access Control Full Server Compromise

πŸ”΄ What Is Broken Access Control? Access Control defines who can do what in an application. When it breaks, a regular user can: Read another user's private data Modify or delete resources they don't own Access admin functions Escalate privileges to full server compromise According to OWASP, 94%...

DE
DEV Community
by CAISD
Broken Access Control Full Server Compromise

πŸ”΄ What Is Broken Access Control?
Access Control defines who can do what in an application. When it breaks, a regular user can:

Read another user's private data
Modify or delete resources they don't own
Access admin functions
Escalate privileges to full server compromise

According to OWASP, 94% of tested applications had some form of broken access control β€” making it the single most dangerous vulnerability class today.

🧠 The Mental Model
Think of your application as a building:

Authentication = The front door lock (are you who you say you are?)
Authorization / Access Control = The internal doors (are you allowed to be here?)

Most developers focus on authentication and forget that every single endpoint, object, and action needs its own authorization check.

πŸ—ΊοΈ Vulnerability Map
Broken Access Control
β”œβ”€β”€ IDOR (Insecure Direct Object Reference)
β”œβ”€β”€ Privilege Escalation
β”‚ β”œβ”€β”€ Horizontal (user β†’ another user)
β”‚ └── Vertical (user β†’ admin)
β”œβ”€β”€ Forced Browsing (accessing unlisted URLs)
β”œβ”€β”€ Missing Function-Level Access Control
β”œβ”€β”€ JWT Attacks
β”‚ β”œβ”€β”€ Algorithm Confusion (RS256 β†’ HS256)
β”‚ β”œβ”€β”€ None Algorithm
β”‚ └── Kid Injection (SQL + Path Traversal)
└── CORS Misconfiguration

πŸ”₯ Attack Type 1: IDOR (Insecure Direct Object Reference)
The most common and easiest to exploit.
Vulnerable Example
httpGET /api/invoices/1042
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
The server returns invoice #1042. What happens if you change the ID?
httpGET /api/invoices/1041 ← Someone else's invoice
GET /api/invoices/1040 ← Another victim
Vulnerable server-side code (Node.js):
javascript// ❌ VULNERABLE β€” No ownership check
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.query(
'SELECT * FROM invoices WHERE id = ?',
[req.params.id]
);
res.json(invoice);
});
Secure fix:
javascript// βœ… SECURE β€” Always check ownership
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.query(
'SELECT * FROM invoices WHERE id = ? AND user_id = ?',
[req.params.id, req.user.id] // ← Bind to authenticated user
);

if (!invoice) return res.status(403).json({ error: 'Forbidden' });
res.json(invoice);
});
Real-World Impact
In 2021, a major banking API exposed this exact pattern. An attacker enumerated sequential IDs and downloaded account statements for thousands of customers. The fix? One extra AND user_id = ? in the query.

πŸ”₯ Attack Type 2: Vertical Privilege Escalation
Regular user becomes Admin.
Vulnerable Example
httpPOST /api/users/update
Content-Type: application/json
Authorization: Bearer

{
"email": "attacker@evil.com",
"role": "admin" ← User-controlled field!
}
Vulnerable code:
javascript// ❌ VULNERABLE β€” Blindly trusts request body
app.post('/api/users/update', authenticate, async (req, res) => {
await db.query(
'UPDATE users SET email=?, role=? WHERE id=?',
[req.body.email, req.body.role, req.user.id] // ← role is attacker-controlled!
);
});
Secure fix:
javascript// βœ… SECURE β€” Never trust user input for privileged fields
app.post('/api/users/update', authenticate, async (req, res) => {
const allowedFields = { email: req.body.email }; // ← Whitelist only safe fields

await db.query(
'UPDATE users SET email=? WHERE id=?',
[allowedFields.email, req.user.id]
);
});

// Role changes require a separate admin-only endpoint
app.post('/api/admin/users/:id/role', authenticate, requireAdmin, async (req, res) => {
// Only admins can reach here
});

πŸ”₯ Attack Type 3: JWT β€” Algorithm Confusion (RS256 β†’ HS256)
This is where it gets advanced.
How JWT Works
Header.Payload.Signature

eyJhbGciOiJSUzI1NiJ9 . eyJ1c2VyX2lkIjoxMDQyfQ .
↑ ↑ ↑
"alg": "RS256" {"user_id": 1042} Server signs with PRIVATE KEY
Server verifies with PUBLIC KEY
The Attack
The server signs tokens with an RSA private key and verifies with the public key.
But what if we change alg to HS256 (symmetric) and sign with the public key?
pythonimport jwt

Attacker obtains the server's public key (often publicly available!)

public_key = open('server_public.pem').read()

Forge a token β€” sign with public key using HS256

malicious_token = jwt.encode(
{"user_id": 1, "role": "admin"}, # ← Escalated privileges
public_key, # ← Using PUBLIC key as HMAC secret
algorithm="HS256" # ← Confused the server
)
Why it works: A vulnerable server does this:
javascript// ❌ VULNERABLE β€” Trusts the algorithm from the token header
const decoded = jwt.verify(token, getKey(token.header.alg));

function getKey(alg) {
if (alg === 'RS256') return rsaPublicKey;
if (alg === 'HS256') return rsaPublicKey; // ← SAME KEY for both!
}
Secure fix:
javascript// βœ… SECURE β€” Algorithm is hardcoded server-side, never from token
const decoded = jwt.verify(token, rsaPublicKey, {
algorithms: ['RS256'] // ← Never trust the token's own alg field
});

πŸ”₯ Attack Type 4: JWT β€” None Algorithm
The most absurd vulnerability in JWT history.
The Attack
Some JWT libraries honor "alg": "none" β€” meaning no signature verification at all.
pythonimport base64, json

header = base64.b64encode(json.dumps({"alg": "none", "typ": "JWT"}).encode()).decode()
payload = base64.b64encode(json.dumps({"user_id": 1, "role": "admin"}).encode()).decode()

No signature needed!

forged_token = f"{header}.{payload}."
Vulnerable code:
javascript// ❌ VULNERABLE β€” Some old libraries accept alg:none
const decoded = jwt.verify(token, secret); // Library honors "none" β†’ no verification
Secure fix:
javascript// βœ… SECURE β€” Explicitly reject "none"
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256', 'RS256'], // ← "none" is not in this list
});

πŸ”₯ Attack Type 5: JWT β€” Kid Injection (Full Server Compromise)
This is the most dangerous JWT attack β€” featured in the CAISD video.
What is kid?
JWT headers can include a kid (Key ID) field that tells the server which key to use for verification:
json{
"alg": "HS256",
"typ": "JWT",
"kid": "key-001"
}
The server looks up kid in a database or filesystem.

Kid SQL Injection
If the server uses kid in a SQL query without sanitization:
javascript// ❌ VULNERABLE β€” kid is used directly in SQL
const secret = await db.query(
SELECT secret FROM keys WHERE kid = '${kid}' // ← SQL Injection!
);
Attacker sets kid to:
json{
"kid": "' UNION SELECT 'attacker_secret'--"
}
The query becomes:
sqlSELECT secret FROM keys WHERE kid = '' UNION SELECT 'attacker_secret'--'
Result: The database returns 'attacker_secret' as the signing key.
Attacker now forges any token they want, signed with 'attacker_secret':
pythonmalicious_kid = "' UNION SELECT 'attacker_secret'--"

header = {"alg": "HS256", "kid": malicious_kid}
payload = {"user_id": 1, "role": "ADMIN"}

forged_token = jwt.encode(payload, 'attacker_secret', algorithm='HS256',
headers=header)
The server verifies the token, gets 'attacker_secret' from DB, and accepts it as valid.

Kid Path Traversal β†’ RCE
If the server uses kid to load a file:
javascript// ❌ VULNERABLE β€” kid used as file path
const keyFile = fs.readFileSync(./keys/${kid});
const secret = keyFile.toString();
Attack 1 β€” Use /dev/null as key (empty string):
json{ "kid": "../../../../dev/null" }
The server reads /dev/null β†’ returns empty string β†’ attacker signs with "":
pythonjwt.encode({"role": "admin"}, "", algorithm="HS256", headers={"kid": "../../../../dev/null"})
Attack 2 β€” Use a known file as key:
json{ "kid": "../../../../etc/hostname" }
If attacker knows the server's hostname, they can use it as the signing secret.

Full Attack Chain: Broken Access Control β†’ Full Server Compromise

  1. Recon
    └─ Discover JWT structure via Burp Suite
    └─ Decode header: {"alg":"HS256", "kid":"key-001"}

  2. Test kid Injection
    └─ kid = "' UNION SELECT 'test'--"
    └─ Sign token with 'test'
    └─ If accepted β†’ SQL Injection confirmed βœ“

  3. Escalate Privileges
    └─ Forge token: {"user_id": 1, "role": "ADMIN"}
    └─ Access /admin panel
    └─ Broken Access Control confirmed βœ“

  4. Pivot to RCE
    └─ As admin, access file upload endpoint
    └─ Upload PHP webshell
    └─ Execute: /uploads/shell.php?cmd=id
    └─ Full Server Compromise βœ“

πŸ”₯ Attack Type 6: Missing Function-Level Access Control
The Scenario
An admin panel is "hidden" but not protected:
httpGET /admin/users β†’ 404 (hidden from menu)
GET /admin/users β†’ 200 (accessible if you know the URL!)
Vulnerable code:
javascript// ❌ VULNERABLE β€” Security through obscurity
app.get('/admin/users', authenticate, async (req, res) => {
// No role check! Just authenticated = enough
const users = await db.query('SELECT * FROM users');
res.json(users);
});
Secure fix:
javascript// βœ… SECURE β€” Explicit role-based authorization
const requireRole = (role) => (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};

app.get('/admin/users', authenticate, requireRole('admin'), async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
});

πŸ”₯ Attack Type 7: Mass Assignment
Trusting user-supplied JSON to update database objects.
Vulnerable Example
httpPATCH /api/profile
Content-Type: application/json

{
"name": "John",
"email": "john@example.com",
"is_admin": true, ← Not in the UI, but accepted!
"account_balance": 999999 ← Financial manipulation
}
Vulnerable code (Ruby on Rails example):
ruby# ❌ VULNERABLE β€” Mass assignment with no filtering
def update
@user.update(params[:user]) # Accepts ALL fields from request
end
Secure fix:
ruby# βœ… SECURE β€” Strong parameters whitelist
def update
@user.update(user_params)
end

private

def user_params
params.require(:user).permit(:name, :email) # Only safe fields
end

πŸ›‘οΈ Defense Checklist
Server-Side Authorization (Non-Negotiable)
βœ… Every endpoint has an explicit authorization check
βœ… Checks are done server-side β€” NEVER trust client
βœ… Object ownership is always verified (user_id match)
βœ… Role checks use server-side session/token data only
βœ… Admin endpoints have their own middleware/guards
JWT Hardening
javascript// βœ… Complete JWT security setup
const jwtOptions = {
algorithms: ['RS256'], // ← Whitelist only
issuer: 'your-app.com', // ← Validate issuer
audience: 'your-app.com', // ← Validate audience
clockTolerance: 30, // ← Max 30s clock skew
};

// βœ… Kid validation β€” never trust raw input
function getSigningKey(kid) {
const ALLOWED_KIDS = new Set(['key-001', 'key-002']);
if (!ALLOWED_KIDS.has(kid)) throw new Error('Invalid kid');
return keyStore.get(kid);
}
RBAC Implementation (Role-Based Access Control)
javascript// βœ… Proper RBAC middleware
const permissions = {
admin: ['read:all', 'write:all', 'delete:all'],
manager: ['read:all', 'write:own'],
user: ['read:own', 'write:own'],
};

const can = (permission) => (req, res, next) => {
const userPerms = permissions[req.user.role] || [];
if (!userPerms.includes(permission)) {
return res.status(403).json({
error: 'Insufficient permissions',
required: permission,
current: req.user.role
});
}
next();
};

// Usage
app.delete('/api/posts/:id', authenticate, can('delete:all'), deletePost);
app.get('/api/profile', authenticate, can('read:own'), getProfile);

πŸ§ͺ Testing Your Application
Manual Testing Checklist

  1. IDOR Testing
    β–‘ Change numeric IDs in requests (1042 β†’ 1041, 1, 9999)
    β–‘ Change UUIDs if predictable
    β–‘ Test with two different user accounts (A reads B's data?)

  2. Privilege Escalation
    β–‘ Add "role":"admin" to profile update requests
    β–‘ Access /admin/* routes with regular user token
    β–‘ Try accessing other users' data with your valid token

  3. JWT Testing (use jwt.io or jwt_tool)
    β–‘ Change alg to "none"
    β–‘ Change alg from RS256 to HS256, sign with public key
    β–‘ Inject SQL into "kid" field
    β–‘ Inject path traversal into "kid" field

  4. Forced Browsing
    β–‘ Fuzz /admin, /internal, /api/v1/admin, /debug
    β–‘ Check JS source files for hidden endpoints
    Automated Tools
    ToolUse CaseBurp SuiteManual testing, JWT editor pluginjwt_toolAutomated JWT attack suiteOWASP ZAPActive scanning for BACAutorize (Burp plugin)Automated IDOR detectionffufForced browsing / directory fuzzing

πŸ“Š Severity Reference
AttackCVSS ScoreBusiness ImpactIDOR (read)7.5 HIGHData breach, regulatory finesIDOR (write/delete)8.1 HIGHData integrity, service disruptionVertical Privilege Escalation9.0 CRITICALFull account takeoverJWT Algorithm Confusion9.8 CRITICALComplete authentication bypassJWT Kid SQLi β†’ RCE10.0 CRITICALFull server compromise

🎯 Key Takeaways

Never trust the client β€” All authorization must be server-side
Check ownership, not just authentication β€” Being logged in β‰  having access to everything
Whitelist, don't blacklist β€” Explicitly define what's allowed, not what's forbidden
JWT is not magic β€” Validate algorithm, kid, issuer, audience β€” every field
Test with two accounts β€” The easiest way to find IDOR is switching accounts and replaying requests

πŸŽ₯ Watch the Full Breakdown

If you want to see these attacks in action, check out the detailed walkthrough by CAISD:

Tags: #security #webdev #hacking #owasp #jwt

DE
Source

This article was originally published by DEV Community and written by CAISD.

Read original article on DEV Community
Back to Discover

Reading List