Shady Oaks Financial: Broken Access Control + Rounding Exploit
Overview
- Platform: BugForge
- Vulnerability: Broken Access Control on admin endpoints; Rounding exploit in stock trading
- Key Technique: Accessing admin-only API routes with a regular user JWT — no role check enforced server-side
- Result: Retrieved flag from
/api/admin/flagas a regular user; generated free money via fractional share rounding
Objective
Find and exploit vulnerabilities in the Shady Oaks Financial stock trading web application.
Initial Access
# Target Application
URL: https://lab-1773951285118-jdyd1f.labs-app.bugforge.io
# Auth details
POST /api/register with {username, email, password}
Returns JWT HS256 Bearer token
Registered as: haxor (id:4, role: user)
Key Findings
-
Broken Access Control on Admin Endpoints (CWE-862: Missing Authorization) — All
/api/admin/*endpoints (flag, stats, users, transactions, stocks) are accessible to any authenticated user regardless of role. The server validates the JWT but never checks therolefield. A regular user can access the flag, view all users, and read all transactions. -
Rounding Exploit in Stock Trading (CWE-187: Partial String Comparison / CWE-682: Incorrect Calculation) — The
POST /api/tradeendpoint does not enforce a minimum transaction cost. Buying 0.001 shares of a cheap stock (e.g., PONZI at ~3.70 EUR) produces a cost of 0.0037 EUR, which rounds to 0.00. Shares are credited despite zero cost, allowing unlimited free share accumulation and eventual sale for profit.
Attack Chain Visualization
┌──────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Register │───▶│ Map API Surface │───▶│ Extract Admin │
│ Account │ │ (Caido + JS │ │ Routes from JS │
│ (id:4) │ │ bundle) │ │ Bundle │
└──────────────┘ └──────────────────┘ └─────────┬───────────┘
│
▼
┌─────────────────────┐
│ GET /api/admin/flag │
│ with regular user │
│ JWT → 200 OK │
│ FLAG RETURNED │
└─────────────────────┘
┌─────────────────── Secondary Finding ───────────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Buy 0.001 shares │───▶│ Cost rounds to │───▶│ Sell accumulated │
│ of PONZI (~3.70) │ │ 0.00 — shares │ │ shares for profit │
│ ~70 times │ │ credited free │ │ +11.19 EUR │
└─────────────────────┘ └──────────────────┘ └─────────────────────┘
Application Architecture
| Component | Detail |
|---|---|
| Backend | Express (Node.js) — X-Powered-By: Express |
| Frontend | React SPA (static/js/main.e2f3b33f.js) |
| Auth | JWT HS256 — payload: {"id":4,"username":"haxor","role":"user","iat":...} |
| CORS | Access-Control-Allow-Origin: * |
API Surface
| Endpoint | Method | Auth | Notes |
|---|---|---|---|
/api/register |
POST | No | Returns JWT + user object |
/api/login |
POST | No | Standard login |
/api/verify-token |
GET | Yes | Token validation |
/api/profile |
PUT | Yes | Whitelisted fields only (full_name, email) |
/api/portfolio |
GET | Yes | User’s stock holdings |
/api/transactions |
GET | Yes | User’s transaction history |
/api/stocks |
GET | Yes | Stock listings |
/api/stocks/:id/history |
GET | Yes | Stock price history |
/api/trade |
POST | Yes | Buy/sell stocks |
/api/currencies |
GET | Yes | Available currencies |
/api/exchange-rates |
GET | Yes | Current exchange rates |
/api/convert-currency |
POST | Yes | Currency conversion |
/api/admin/flag |
GET | Yes | No role check — VULN |
/api/admin/stats |
GET | Yes | No role check — VULN |
/api/admin/users |
GET | Yes | No role check — VULN |
/api/admin/transactions |
GET | Yes | No role check — VULN |
/api/admin/stocks |
GET | Yes | No role check — VULN |
Exploitation Path
Step 1: Reconnaissance — Registration and API Enumeration
Registered an account and explored the application through Caido, capturing all HTTP requests. The app is a stock trading platform with support for buying/selling stocks, currency conversion, and portfolio management.
POST /api/register HTTP/1.1
Content-Type: application/json
{
"username": "haxor",
"email": "haxor@test.com",
"password": "password123"
}
Response returned a JWT with payload {"id":4,"username":"haxor","role":"user","iat":...}. The role field in the JWT and the existence of user IDs 1-3 indicated an admin role likely exists.
Step 2: JavaScript Bundle Analysis — Admin Route Discovery
Extracted API routes from the React JS bundle (static/js/main.e2f3b33f.js). Found references to admin endpoints not exposed in the regular UI:
/api/admin/flag/api/admin/stats/api/admin/users/api/admin/transactions/api/admin/stocks
Step 3: Broken Access Control — Admin Endpoints Accessible as Regular User
Tested the admin flag endpoint using the regular user JWT:
GET /api/admin/flag HTTP/1.1
Authorization: Bearer <haxor_jwt_with_role_user>
Response: 200 OK
{"flag":"bug{vjsRfxts5fAaDF1hkkZmhzd8NpW74xCT}"}
No role check performed. Also confirmed /api/admin/stats returns full system statistics (4 users, all stock data, total cash) with the same regular user token.
Step 4: Rounding Exploit — Free Share Accumulation
Discovered that buying very small fractional shares of cheap stocks results in zero cost:
POST /api/trade HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json
{"stock_id":3,"shares":0.001,"action":"buy"}
Response:
{"total_cost":"0.00","new_balance":"987.52","shares":"0.0010"}
The cost of 0.001 shares of PONZI (~3.70 EUR/share) is 0.0037 EUR, which rounds to 0.00. Shares are credited despite zero cost.
Repeated ~70 times — balance remained at 987.52 throughout. Accumulated shares from 3.001 to 3.07.
Step 5: Profit Realization — Selling Free Shares
Sold 3 shares of the accumulated PONZI stock:
POST /api/trade HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json
{"stock_id":3,"shares":3,"action":"sell"}
Balance increased from 987.52 to 998.71 — a profit of 11.19 EUR generated from nothing.
Scope note: This exploit is price-dependent. It works on stocks where price * 0.001 < 0.005 (rounds to 0.00). On expensive stocks like 404EX at 116.23/share, 0.001 shares costs 0.12 — correctly rounded up and charged.
Flag / Objective Achieved
| Vector | Flag |
|---|---|
Broken Access Control on /api/admin/flag |
bug{vjsRfxts5fAaDF1hkkZmhzd8NpW74xCT} |
Key Learnings
- Authentication is not authorization. The admin endpoints required a valid JWT (authentication) but never checked whether the user had the admin role (authorization). These are separate concerns that must both be implemented.
- Client-side route hiding is not security. The admin routes were absent from the regular UI but present in the JS bundle. Hiding routes in the frontend provides zero protection — the server must enforce access control.
- Always review JS bundles for hidden endpoints. React SPAs compile all route definitions into the JavaScript bundle, making them trivially extractable even when not rendered in the UI.
- Rounding errors in financial calculations enable money generation. When fractional transactions round to zero but the system still processes them, an attacker can accumulate assets for free. Financial systems must enforce minimum transaction amounts and use proper decimal arithmetic.
- Test all price tiers for rounding exploits. The vulnerability is price-dependent — it works on cheap stocks but not expensive ones. Testing only one stock could miss the issue.
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
Mass assignment on PUT /api/profile (injecting role: "admin") |
Extra fields rejected | Server whitelists fields — only full_name and email accepted |
| Negative values on /api/trade | 400 Bad Request | Server validates that amounts must be positive |
| Negative values on /api/convert-currency | 400 Bad Request | Server validates that amounts must be positive |
| Same-currency conversion (EUR → EUR) | Rejected | Server correctly blocks same-currency conversion |
Tools Used
| Tool | Purpose |
|---|---|
| Caido | HTTP proxy — API enumeration, request interception and replay |
| Browser DevTools | React SPA analysis, JS bundle route extraction |
| curl | Direct API endpoint testing |
Remediation
1. Missing Authorization on Admin Endpoints (CVSS: 9.8 - Critical)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Issue: All /api/admin/* endpoints are accessible to any authenticated user. The server validates the JWT but does not check the role field before processing admin requests. This exposes the flag, all user data, all transactions, and system statistics.
CWE Reference: CWE-862 — Missing Authorization
Fix:
// BEFORE (Vulnerable)
router.get('/admin/flag', authMiddleware, (req, res) => {
res.json({ flag: process.env.FLAG });
});
// AFTER (Secure)
const requireAdmin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
router.get('/admin/flag', authMiddleware, requireAdmin, (req, res) => {
res.json({ flag: process.env.FLAG });
});
// Apply to all admin routes
router.use('/admin', authMiddleware, requireAdmin);
Additional recommendations:
- Do not rely on the JWT
roleclaim alone — verify role from the database on each admin request to prevent JWT forgery - Implement role-based access control (RBAC) middleware applied globally to all
/admin/*routes - Log all admin endpoint access attempts for security monitoring
2. Rounding Exploit in Stock Trading (CVSS: 6.5 - Medium)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
Issue: The trading endpoint does not enforce a minimum transaction cost. When the calculated cost rounds to 0.00, shares are still credited, allowing unlimited free accumulation.
CWE Reference: CWE-682 — Incorrect Calculation
Fix:
// BEFORE (Vulnerable)
const totalCost = (shares * price).toFixed(2);
// totalCost can be "0.00" but shares still credited
// AFTER (Secure)
const totalCost = shares * price;
const MIN_TRANSACTION = 0.01;
if (totalCost < MIN_TRANSACTION) {
return res.status(400).json({
error: `Minimum transaction amount is ${MIN_TRANSACTION} EUR`
});
}
// Use decimal library for precision
const Decimal = require('decimal.js');
const preciseCost = new Decimal(shares).times(new Decimal(price));
Additional recommendations:
- Enforce minimum share quantity (e.g., 0.01 shares minimum)
- Use a decimal arithmetic library (decimal.js, big.js) instead of floating-point for all financial calculations
- Round transaction costs up (ceiling), never down, to prevent zero-cost accumulation
OWASP Top 10 Coverage
- A01:2021 — Broken Access Control: The primary vulnerability. Admin endpoints enforce authentication but not authorization, allowing any authenticated user to access admin-only resources including the flag, user data, and system statistics.
- A04:2021 — Insecure Design: Admin routes lack a systematic authorization layer. Role checks should be enforced at the middleware level for all admin routes, not on individual endpoints.
- A08:2021 — Software and Data Integrity Failures: The rounding exploit stems from insufficient validation of financial calculation outputs. The system trusts floating-point arithmetic results without checking for edge cases like zero-cost transactions.
References
- CWE-862: Missing Authorization
- CWE-682: Incorrect Calculation
- OWASP Top 10:2021 — Broken Access Control
- OWASP Testing Guide: Authorization Testing
- OWASP API Security: Broken Function Level Authorization
Tags: #broken-access-control #missing-authorization #rounding-exploit #financial-logic #bugforge #webapp
Document Version: 1.0
Last Updated: 2026-03-19