Skip to content

Commit fd9ec03

Browse files
Merge pull request #68 from snoopysecurity/new-vulns
feat: add new vulns
2 parents b1b4daf + 59723c1 commit fd9ec03

26 files changed

Lines changed: 1295 additions & 173 deletions

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ This vulnerable application contains the following API/Web Service vulnerabiliti
3232
* GraphQL Introspection Enabled
3333
* GraphQL Arbitrary File Write
3434
* GraphQL Batching Brute Force
35+
* API Endpoint Brute Forcing
36+
* CRLF Injection
37+
* XML Injection
38+
* XML Bomb Denial-of-Service
39+
* SOAP Injection
40+
* Cross-Site Request Forgery (CSRF)
3541
* Client Side Template Injection
3642

3743
## Set Up Instructions
@@ -69,7 +75,7 @@ Change directory to DVWS
6975
cd dvws-node
7076
```
7177

72-
npm install all dependencies (build from source is needed for `libxmljs`, you might also need install libxml depending on your OS: `sudo apt-get install -y libxml2 libxml2-dev`)
78+
npm install all dependencies (build from source is needed for `libxmljs`, you might also need to install libxml depending on your OS: `sudo apt-get install -y libxml2 libxml2-dev`)
7379

7480

7581
```
@@ -126,16 +132,11 @@ If the DVWS web service doesn't start because of delayed MongoDB or MySQL setup,
126132

127133

128134
## To Do
129-
* Cross-Site Request Forgery (CSRF)
130-
* XML Bomb Denial-of-Service
131-
* API Endpoint Brute Forcing
135+
132136
* Web Socket Security
133137
* Type Confusion
134138
* LDAP Injection
135-
* SOAP Injection
136-
* XML Injection
137139
* GRAPHQL Denial Of Service
138-
* CRLF Injection
139140
* GraphQL Injection
140141
* Webhook security
141142

app.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ swaggerGen().then(() => {
7171

7272
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerOutput));
7373

74-
app.listen(process.env.EXPRESS_JS_PORT, '0.0.0.0', () => {
74+
const serverInstance = app.listen(process.env.EXPRESS_JS_PORT, '0.0.0.0', () => {
7575
console.log(`🚀 API listening at http://dvws.local${process.env.EXPRESS_JS_PORT == 80 ? "" : ":" + process.env.EXPRESS_JS_PORT } (127.0.0.1)`);
7676
});
77+
}).catch(err => {
78+
console.error("Unable to generate Swagger documentation", err);
79+
process.exit(1);
7780
});
7881

7982

controllers/notebook.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const jwt = require('jsonwebtoken')
33
const { exec } = require('child_process');
44
var xpath = require('xpath');
55
const xml2js = require('xml2js');
6+
const libxml = require('libxmljs');
67
const fs = require('fs');
78
dom = require('@xmldom/xmldom').DOMParser
89
const parser = new xml2js.Parser({ attrkey: "ATTR" });
@@ -200,6 +201,61 @@ module.exports = {
200201
} finally {
201202
await client.close();
202203
}
204+
},
205+
206+
// Vulnerability: XML Bomb / XXE (Import Notes)
207+
import_notes_xml: async (req, res) => {
208+
res = set_cors(req, res);
209+
210+
const xmlData = req.body.xml;
211+
if (!xmlData) {
212+
return res.status(400).send({ error: "XML data required" });
213+
}
214+
215+
// Verify token
216+
let result = {};
217+
try {
218+
const token = req.headers.authorization.split(' ')[1];
219+
result = jwt.verify(token, process.env.JWT_SECRET, options);
220+
} catch (e) {
221+
return res.status(401).send({ error: "Unauthorized" });
222+
}
223+
224+
const optionsXml = {
225+
noent: true, // VULNERABLE: Enables entity substitution
226+
dtdload: true,
227+
huge: true // VULNERABLE: Bypasses parser limits (e.g. max node depth) to facilitate DoS
228+
};
229+
230+
try {
231+
const doc = libxml.parseXml(xmlData, optionsXml);
232+
233+
// Parse and save notes
234+
const notes = doc.find('//note');
235+
let count = 0;
236+
237+
for (const node of notes) {
238+
const name = node.get('name') ? node.get('name').text() : ("Imported " + Date.now());
239+
const body = node.get('body') ? node.get('body').text() : "";
240+
const type = node.get('type') ? node.get('type').text() : "public";
241+
242+
const newNote = new Note({
243+
name: name,
244+
body: body,
245+
type: type,
246+
user: result.user
247+
});
248+
await newNote.save();
249+
count++;
250+
}
251+
252+
res.send({
253+
success: true,
254+
message: `Successfully imported ${count} notes.`,
255+
parsedRoot: doc.root().name()
256+
});
257+
} catch (e) {
258+
res.status(500).send(e);
259+
}
203260
}
204-
205261
}

controllers/passphrase.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const jwt = require('jsonwebtoken');
44
var serialize = require("node-serialize")
55
const PDFDocument = require('pdfkit');
66
const fs = require('fs');
7+
const bcrypt = require('bcrypt');
8+
const User = require('../models/users');
79

810
const sequelize = require('../models/passphrase');
911

@@ -71,6 +73,23 @@ const options = {
7173
let result = {};
7274
const token = req.headers.authorization.split(' ')[1];
7375
result = jwt.verify(token, process.env.JWT_SECRET, options);
76+
77+
// Verify credentials before export (Vulnerable: No Rate Limiting + User enumeration)
78+
const { password, username } = req.body;
79+
if (!password || !username) {
80+
return res.status(400).send("Username and Password required");
81+
}
82+
83+
try {
84+
// Vulnerability: Uses username from body allowing brute force of any user
85+
const user = await User.findOne({ username: username });
86+
if (!user || !(await bcrypt.compare(password, user.password))) {
87+
return res.status(401).send("Incorrect credentials");
88+
}
89+
} catch (err) {
90+
return res.status(500).send(err.message);
91+
}
92+
7493
const payload = Buffer.from(req.body.data, 'base64');
7594
const data = serialize.unserialize(payload.toString());
7695

controllers/users.js

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
const mongoose = require('mongoose');
22
const bcrypt = require('bcrypt');
33
const jwt = require('jsonwebtoken');
4+
const xml2js = require('xml2js');
45

56
const connUri = process.env.MONGO_LOCAL_CONN_URL;
67
const User = require('../models/users');
78

9+
const options = {
10+
expiresIn: '2d',
11+
issuer: 'https://github.com/snoopysecurity',
12+
algorithms: ["HS256", "none"],
13+
ignoreExpiration: true
14+
};
15+
16+
// In-memory log store for login attempts (Vulnerable to Log Pollution)
17+
const loginLogs = [];
18+
819
function set_cors(req,res) {
920
if (req.get('origin')) {
1021
res.header('Access-Control-Allow-Origin', req.get('origin'))
@@ -97,6 +108,12 @@ module.exports = {
97108
let result = {};
98109
let status = 200;
99110

111+
// Vulnerability: Log Pollution via CRLF Injection
112+
// We log the username directly without sanitization.
113+
// If username contains \n, it creates a fake log entry on a new line.
114+
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || "unknown";
115+
loginLogs.push(`[${new Date().toISOString()}] Login attempt from IP:${ip} User:${username}`);
116+
100117
try {
101118
const user = await User.findOne({username});
102119
if (user) {
@@ -140,7 +157,8 @@ module.exports = {
140157
result.error = `Authentication error`;
141158
}
142159
res.setHeader('Authorization', 'Bearer '+ result.token);
143-
//res.cookie("SESSIONID", result.token, {httpOnly:true, secure:true});
160+
// Set cookie for CSRF demonstration
161+
res.setHeader('Set-Cookie', `auth_token=${result.token}; Path=/; HttpOnly`);
144162
res.status(status).send(result);
145163
} catch (err) {
146164
status = 500;
@@ -178,5 +196,184 @@ module.exports = {
178196
result.error = err;
179197
}
180198
res.status(status).send(result);
199+
},
200+
201+
getLoginLogs: (req, res) => {
202+
// Returns raw logs. Vulnerable to Log Pollution/Forgery if displayed line-by-line.
203+
res.set('Content-Type', 'text/plain');
204+
res.send(loginLogs.join('\n'));
205+
},
206+
207+
208+
// Vulnerability: XML Injection (Profile Export)
209+
exportProfileXml: async (req, res) => {
210+
// Scenario: User exports their profile to XML.
211+
// Vulnerability: The 'bio' and 'username' fields are user-controlled and concatenated directly.
212+
const username = req.body.username || "guest";
213+
const bio = req.body.bio || "No bio";
214+
215+
// Construct XML manually (Vulnerable)
216+
const xml = `
217+
<userProfile>
218+
<username>${username}</username>
219+
<role>user</role>
220+
<bio>${bio}</bio>
221+
</userProfile>
222+
`;
223+
224+
res.set('Content-Type', 'application/xml');
225+
res.send(xml);
226+
},
227+
228+
// Vulnerability: XML Injection (Profile Import - Mass Assignment)
229+
importProfileXml: async (req, res) => {
230+
// Scenario: User imports profile from XML.
231+
// Vulnerability: The endpoint blindly accepts fields from the XML.
232+
// Mass Assignment: If XML contains <admin>true</admin>, user becomes admin.
233+
234+
const xmlData = req.body.xml;
235+
if (!xmlData) return res.status(400).send("XML required");
236+
237+
try {
238+
const parser = new xml2js.Parser({ explicitArray: false });
239+
const result = await parser.parseStringPromise(xmlData);
240+
241+
if (result && result.userProfile) {
242+
const profile = result.userProfile;
243+
const targetUser = profile.username;
244+
245+
// Build update object
246+
const updateData = {};
247+
if (profile.bio) updateData.bio = profile.bio;
248+
// Vulnerability: Accepting admin flag from XML
249+
if (profile.admin) updateData.admin = (profile.admin === 'true');
250+
251+
const updatedUser = await User.findOneAndUpdate(
252+
{ username: targetUser },
253+
updateData,
254+
{ new: true }
255+
);
256+
257+
if (!updatedUser) {
258+
return res.status(404).send({ success: false, message: "Target user '" + targetUser + "' not found." });
259+
}
260+
261+
res.send({
262+
success: true,
263+
message: "Profile updated successfully from XML.",
264+
data: updatedUser
265+
});
266+
} else {
267+
res.status(400).send("Invalid XML format. Root must be <userProfile>");
268+
}
269+
} catch (e) {
270+
res.status(500).send("XML Import Error: " + e.message);
271+
}
272+
},
273+
274+
getProfile: async (req, res) => {
275+
try {
276+
const token = req.headers.authorization.split(' ')[1];
277+
const decoded = jwt.verify(token, process.env.JWT_SECRET, options);
278+
279+
const user = await User.findOne({ username: decoded.user });
280+
if (!user) return res.status(404).send("User not found");
281+
282+
res.send({
283+
username: user.username,
284+
bio: user.bio,
285+
admin: user.admin
286+
});
287+
} catch (err) {
288+
res.status(500).send(err.message);
289+
}
290+
},
291+
292+
adminCreateUser: async (req, res) => {
293+
try {
294+
295+
let token;
296+
if (req.headers.cookie) {
297+
const cookies = req.headers.cookie.split(';');
298+
const authCookie = cookies.find(c => c.trim().startsWith('auth_token='));
299+
if (authCookie) token = authCookie.split('=')[1];
300+
}
301+
302+
if (!token) return res.status(401).send({ error: "Unauthorized" });
303+
304+
// Verify token is Admin
305+
const decoded = jwt.verify(token, process.env.JWT_SECRET, options);
306+
const user = await User.findOne({ username: decoded.user });
307+
if (!user || !user.admin) return res.status(403).send({ error: "Forbidden: Admin only" });
308+
309+
// 2. Parse Body (Parses JSON even if Content-Type is text/plain)
310+
let data = req.body;
311+
if (typeof data === 'string') {
312+
try {
313+
data = JSON.parse(data);
314+
} catch (e) { /* ignore */ }
315+
}
316+
317+
// 3. Create User
318+
if (data && data.username && data.password) {
319+
const existing = await User.findOne({ username: data.username });
320+
if (existing) return res.status(409).send({ error: "User already exists" });
321+
322+
const newUser = new User({
323+
username: data.username,
324+
password: data.password,
325+
admin: !!data.admin
326+
});
327+
await newUser.save();
328+
res.status(200).send({ message: `User ${data.username} created successfully.` });
329+
} else {
330+
res.status(400).send({ error: "Missing username or password" });
331+
}
332+
} catch (err) {
333+
res.status(500).send({ error: err.message });
334+
}
335+
},
336+
337+
338+
339+
// Vulnerability: LDAP Injection
340+
ldapSearch: (req, res) => {
341+
const user = req.query.user || req.body.user;
342+
343+
// Vulnerability: Unsanitized input concatenated into LDAP filter
344+
// Standard filter: (uid=username)
345+
const filter = "(uid=" + user + ")";
346+
347+
// Simulated LDAP Server Logic
348+
let results = [];
349+
350+
// 1. Wildcard Injection: user = "*"
351+
if (user === "*" || filter.includes("(uid=*)")) {
352+
results = ["admin", "guest", "manager"];
353+
}
354+
// 2. Attribute Injection: user = "admin)(objectClass=*)"
355+
// Filter becomes: (uid=admin)(objectClass=*)
356+
else if (filter.includes(")(objectClass=*)")) {
357+
// Vulnerability Impact: By injecting a valid second filter, the attacker might bypass field restrictions
358+
// or trigger a verbose mode, revealing sensitive attributes normally hidden.
359+
results = [
360+
{
361+
username: "admin",
362+
email: "admin@internal.dvws",
363+
guid: "a1b2-c3d4-e5f6",
364+
description: "Super User with unrestricted access",
365+
password: "letmein"
366+
}
367+
];
368+
}
369+
// Normal match
370+
else if (user === "admin") {
371+
results = ["admin"];
372+
}
373+
374+
res.status(200).send({
375+
filter: filter, // Reflect filter for educational/debugging
376+
results: results
377+
});
181378
}
182379
};

models/users.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ const userSchema = new Schema({
2727
admin: {
2828
type: Boolean,
2929
default: false
30+
},
31+
bio: {
32+
type: 'String',
33+
required: false,
34+
default: "No bio yet."
3035
}
3136
});
3237

0 commit comments

Comments
 (0)