|
1 | 1 | const mongoose = require('mongoose'); |
2 | 2 | const bcrypt = require('bcrypt'); |
3 | 3 | const jwt = require('jsonwebtoken'); |
| 4 | +const xml2js = require('xml2js'); |
4 | 5 |
|
5 | 6 | const connUri = process.env.MONGO_LOCAL_CONN_URL; |
6 | 7 | const User = require('../models/users'); |
7 | 8 |
|
| 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 | + |
8 | 19 | function set_cors(req,res) { |
9 | 20 | if (req.get('origin')) { |
10 | 21 | res.header('Access-Control-Allow-Origin', req.get('origin')) |
@@ -97,6 +108,12 @@ module.exports = { |
97 | 108 | let result = {}; |
98 | 109 | let status = 200; |
99 | 110 |
|
| 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 | + |
100 | 117 | try { |
101 | 118 | const user = await User.findOne({username}); |
102 | 119 | if (user) { |
@@ -140,7 +157,8 @@ module.exports = { |
140 | 157 | result.error = `Authentication error`; |
141 | 158 | } |
142 | 159 | 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`); |
144 | 162 | res.status(status).send(result); |
145 | 163 | } catch (err) { |
146 | 164 | status = 500; |
@@ -178,5 +196,184 @@ module.exports = { |
178 | 196 | result.error = err; |
179 | 197 | } |
180 | 198 | 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 | + }); |
181 | 378 | } |
182 | 379 | }; |
0 commit comments