Introduction: Becoming the Architect of Vulnerabilities
Welcome to Chapter 19! So far in our journey through advanced web application security, we’ve explored deep exploitation techniques, chained vulnerabilities, business logic flaws, and various bypasses for XSS and CSRF. We’ve dissected authentication failures, token attacks, API abuse, and even touched upon modern frontend attack surfaces. Now, it’s time to flip the script and step into the shoes of the creator of insecure systems.
In this chapter, we’ll learn how to build our own intentionally vulnerable web application demo projects. Why do this? Because truly understanding how real attackers exploit systems requires more than just identifying vulnerabilities; it demands an intimate knowledge of how those vulnerabilities are introduced and why they exist in the first place. By constructing insecure applications from the ground up, you’ll gain unparalleled insight into the attacker’s mental model, solidify your grasp on prevention techniques, and sharpen your detection skills. This hands-on approach will bridge the gap between theoretical knowledge and practical application, making you a more effective security professional.
Before we dive in, ensure you’re comfortable with basic web development concepts (like HTTP requests, server-side logic, and client-side scripting) and have a foundational understanding of common web vulnerabilities covered in earlier chapters. Let’s get building!
Core Concepts: The Art of Intentional Insecurity
Building a vulnerable application isn’t about writing “bad” code randomly; it’s a deliberate and educational process. Our goal is to create a controlled environment where we can safely experiment with exploitation techniques and then, crucially, learn how to fix them.
Why Build, Not Just Use?
While there are many excellent pre-built vulnerable applications (like OWASP Juice Shop or DVWA), creating your own offers unique benefits:
- Deeper Understanding: You choose exactly which vulnerabilities to implement, how subtle or obvious they are, and where they reside within the application’s logic. This forces you to think about the root cause of security flaws.
- Customization: Tailor the application to specific learning goals. Want to practice a complex chained attack involving a unique business logic flaw? Build it!
- Tech Stack Familiarity: Work with a technology stack you’re comfortable with (or want to learn), allowing you to understand how vulnerabilities manifest in different languages and frameworks.
- Debugging & Remediation: Once you’ve exploited your own creation, you’re perfectly positioned to implement and test various defense mechanisms, truly understanding their impact.
The Ethical Hacking Mental Model (Reversed)
Traditionally, ethical hacking involves identifying vulnerabilities in someone else’s code. When building a vulnerable app, you adopt a reversed mental model:
- Design Flaws: Intentionally design components with security weaknesses.
- Implement Vulnerably: Write code that directly introduces the chosen vulnerability.
- Document: Clearly note where and what type of vulnerability exists.
- Exploit: Switch hats and try to exploit your own creation.
- Remediate: Fix the vulnerability using secure coding practices.
- Verify: Ensure the fix works and hasn’t introduced new issues.
This iterative process is invaluable for truly internalizing secure design patterns and understanding attack vectors.
Choosing Your Tech Stack
For our demo projects, simplicity and quick setup are key. Common choices include:
- Backend: Node.js with Express (lightweight, JavaScript-based), Python with Flask/Django (popular for web dev), PHP with Laravel/Symfony (widespread legacy applications), Java with Spring Boot (enterprise-grade).
- Frontend: For simpler demos, plain HTML/CSS/JavaScript is often sufficient. If you want to explore modern frontend attack surfaces (like in React or Angular applications), you’d build a separate frontend project that interacts with your vulnerable backend API.
For our practical example, we’ll use Node.js with Express, as it’s quick to set up and widely used.
Types of Vulnerabilities to Embed
Think of the OWASP Top 10 as your inspiration! You can embed:
- Injection Flaws: SQL Injection, NoSQL Injection, Command Injection.
- Broken Authentication & Authorization: Weak password handling, session fixation, IDOR (Insecure Direct Object Reference).
- Cross-Site Scripting (XSS): Reflected, Stored, DOM-based.
- Cross-Site Request Forgery (CSRF): Lack of anti-CSRF tokens.
- Security Misconfigurations: Default credentials, exposed directories.
- Business Logic Flaws: Price manipulation, workflow bypasses.
- API Abuse: Rate limiting bypasses, excessive data exposure.
Today, we’ll start with a couple of straightforward but impactful vulnerabilities to get the ball rolling.
Step-by-Step Implementation: Building a Vulnerable Node.js App
Let’s create a super simple Node.js application that will intentionally contain a couple of common vulnerabilities. We’ll use Node.js v21.x (or the latest LTS v20.x, current as of January 2026) and Express v4.x.
Step 1: Initialize Your Project
First, create a new directory for your project and initialize a Node.js project.
Create Project Folder:
mkdir vulnerable-app-demo cd vulnerable-app-demoInitialize npm: This creates a
package.jsonfile.npm init -yExplanation:
npm init -yquickly initializes a new Node.js project, creating apackage.jsonfile with default values. This file manages our project’s metadata and dependencies.Install Express:
npm install [email protected] # Using a specific stable version for consistencyExplanation: We install the Express framework, which simplifies building web servers in Node.js.
4.18.2is a recent stable version. You can check the latest stable release on the official npm registry or Express documentation if needed. As of 2026-01-04, Express 4.x is still the dominant stable series, with 5.x in beta.
Step 2: Create a Basic Server
Now, let’s create our main application file, index.js, and set up a basic Express server.
Create
index.js:touch index.jsAdd Basic Server Code: Open
index.jsand add the following:// index.js const express = require('express'); // Import the Express library const app = express(); // Create an Express application instance const port = 3000; // Define the port our server will listen on // Middleware to parse URL-encoded bodies (for form submissions) app.use(express.urlencoded({ extended: true })); // Middleware to parse JSON bodies (for API requests) app.use(express.json()); // Our "database" for demonstration purposes const users = { '101': { id: '101', username: 'alice', email: '[email protected]', role: 'user' }, '102': { id: '102', username: 'bob', email: '[email protected]', role: 'admin' }, '103': { id: '103', username: 'charlie', email: '[email protected]', role: 'user' } }; // --- Routes will go here --- // Start the server app.listen(port, () => { console.log(`Vulnerable app listening at http://localhost:${port}`); console.log(`Try visiting:`); console.log(`- http://localhost:${port}/profile/101`); console.log(`- http://localhost:${port}/search?query=hello`); });Explanation:
const express = require('express');: This line imports the Express module, making its functionalities available to our application.const app = express();: We create an instance of the Express application. Thisappobject will be used to define routes and middleware.const port = 3000;: We define the port number on which our server will listen for incoming requests.app.use(express.urlencoded({ extended: true }));andapp.use(express.json());: These are Express middleware functions. They allow our application to automatically parse incoming request bodies, making it easier to work with form data (urlencoded) and JSON data (json).const users = { ... };: This is a simple JavaScript object acting as our in-memory “database” for user data. In a real application, this would be a proper database.app.listen(port, ...);: This starts our Express server, making it listen for requests on the specified port. The callback function runs once the server is successfully started.
Run the Server (and confirm it works):
node index.jsYou should see the console output:
Vulnerable app listening at http://localhost:3000 Try visiting: - http://localhost:3000/profile/101 - http://localhost:3000/search?query=helloYou can visit
http://localhost:3000in your browser, but it will currently show “Cannot GET /” because we haven’t defined any root route yet. That’s okay!
Step 3: Implement an Insecure Direct Object Reference (IDOR)
Now for our first vulnerability! We’ll create a profile page that allows users to view their details, but we’ll make it vulnerable to IDOR.
Add IDOR Vulnerability to
index.js: Insert this code beforeapp.listen():// Vulnerable IDOR endpoint app.get('/profile/:id', (req, res) => { const userId = req.params.id; // Get user ID directly from URL parameter // In a real app, you'd check if the *authenticated* user has permission // to view this userId. Here, we INTENTIONALLY skip that check. const user = users[userId]; if (user) { res.status(200).send(` <h1>User Profile for ${user.username}</h1> <p>ID: ${user.id}</p> <p>Email: ${user.email}</p> <p>Role: ${user.role}</p> <p>You are viewing profile ID: ${userId}</p> `); } else { res.status(404).send('User not found.'); } });Explanation:
app.get('/profile/:id', ...): This defines a GET route where:idis a route parameter. When a user visits/profile/101,req.params.idwill be'101'.const userId = req.params.id;: We directly take the user ID from the URL.const user = users[userId];: We use this ID to fetch user data from ourusersobject.- The Vulnerability: The critical flaw here is the lack of an authorization check. Any user, authenticated or not, can simply change the
idin the URL (e.g.,/profile/102) and view any other user’s profile data. This is a classic Insecure Direct Object Reference (IDOR). - We intentionally send back some basic HTML to make it easy to see the data.
Restart the server: Stop the previous
node index.jsprocess (usually Ctrl+C) and runnode index.jsagain.Test the IDOR:
- Open your browser and navigate to
http://localhost:3000/profile/101. You should see Alice’s profile. - Now, change the URL to
http://localhost:3000/profile/102. You should see Bob’s profile, even though you’re not logged in as Bob or authorized to view his data! This demonstrates the IDOR. - Try
http://localhost:3000/profile/103for Charlie.
- Open your browser and navigate to
Step 4: Implement a Basic Reflected Cross-Site Scripting (XSS)
Next, let’s introduce an XSS vulnerability through a simple search feature.
Add Reflected XSS Vulnerability to
index.js: Insert this code beforeapp.listen()and after the IDOR route:// Vulnerable Reflected XSS endpoint app.get('/search', (req, res) => { const query = req.query.query; // Get search query directly from URL parameter if (query) { // Here, we INTENTIONALLY reflect user input directly into the HTML // without any sanitization or encoding. res.status(200).send(` <h1>Search Results</h1> <p>You searched for: ${query}</p> <div id="results">No results found for "${query}"</div> <hr> <p>This search feature is not implemented securely yet.</p> `); } else { res.status(200).send(` <h1>Search Page</h1> <p>Enter a query to search.</p> <form action="/search" method="GET"> <input type="text" name="query" placeholder="Your search query"> <button type="submit">Search</button> </form> `); } });Explanation:
app.get('/search', ...): This defines a GET route for a search page.const query = req.query.query;: We extract thequeryparameter from the URL’s query string (e.g.,?query=something).- The Vulnerability: When
queryis present, the application directly embeds the value ofqueryinto the HTML response using${query}. There is no HTML entity encoding or sanitization applied. This is the hallmark of a Reflected XSS vulnerability.
Restart the server: Stop the previous
node index.jsprocess and runnode index.jsagain.Test the Reflected XSS:
- Open your browser and navigate to
http://localhost:3000/search?query=hello. You should see “You searched for: hello”. - Now, try an XSS payload:
http://localhost:3000/search?query=<script>alert('XSSed!')</script> - What happened? You should see a JavaScript alert box pop up with “XSSed!”. This confirms the Reflected XSS vulnerability.
- Try another payload:
http://localhost:3000/search?query=<img src=x onerror=alert('Image%20XSS')>(Note:%20is URL encoding for a space). This should also trigger an alert.
- Open your browser and navigate to
Congratulations! You’ve just built a simple web application with two common vulnerabilities. This is your canvas for learning and experimentation.
Mini-Challenge: Another IDOR Instance
You’ve seen how easily an IDOR can sneak into a user profile route. Now, let’s challenge you to find another place for it.
Challenge: Add a new endpoint to our vulnerable-app-demo that simulates an “order details” page. This page should also be vulnerable to IDOR, allowing any user to view any other user’s order details by simply changing an orderId in the URL.
Hint:
- You’ll need a simple in-memory
ordersobject, similar to ourusersobject. - The route structure will be very similar to the
/profile/:idroute. - Remember to intentionally omit any authorization checks!
What to observe/learn: How common it is for IDORs to appear in features that retrieve specific resources based on an ID, especially when proper access control is overlooked.
Common Pitfalls & Troubleshooting
Building vulnerable applications has its own unique set of challenges!
- Accidentally Being Too Secure: It’s ingrained in developers to write secure code. Sometimes, you might inadvertently use a framework feature that automatically prevents XSS (like templating engines that auto-escape) or implement a default authorization check without realizing it.
- Troubleshooting: If a vulnerability isn’t working as expected, double-check your code against the specific vulnerability’s mechanics. Are you truly reflecting input unsanitized? Is there really no authorization logic? Sometimes, using raw
res.send()orres.write()with string concatenation (as we did for XSS) helps bypass default protections.
- Troubleshooting: If a vulnerability isn’t working as expected, double-check your code against the specific vulnerability’s mechanics. Are you truly reflecting input unsanitized? Is there really no authorization logic? Sometimes, using raw
- Over-Complicating the Project: It’s easy to get carried away building a complex application. Remember, the goal is to demonstrate specific vulnerabilities, not to build a production-ready app.
- Troubleshooting: Keep your vulnerable features isolated and minimal. Focus on one vulnerability at a time or a small chain, then move on. If the project gets too big, consider breaking it into smaller, separate vulnerable demos.
- Not Documenting Vulnerabilities: You build it, you exploit it, you fix it… but then you forget where the original vulnerability was or how it worked.
- Troubleshooting: Add comments directly in your code explaining why a particular line or block is vulnerable. You can even create a
README.mdfile detailing each vulnerability, its expected exploit, and where to find it. This is crucial for future reference and for sharing your learning lab.
- Troubleshooting: Add comments directly in your code explaining why a particular line or block is vulnerable. You can even create a
Summary: Your Personal Security Playground
In this chapter, we shifted our perspective from simply identifying vulnerabilities to actively creating them. You learned:
- The value of building intentionally vulnerable applications for deep understanding, customization, and practical skill development.
- The reversed ethical hacking mental model of designing, implementing, exploiting, and remediating your own flaws.
- How to set up a basic Node.js/Express application (as of 2026-01-04, using Node.js v21.x/v20.x and Express v4.x).
- Step-by-step implementation of an Insecure Direct Object Reference (IDOR) vulnerability.
- Step-by-step implementation of a Reflected Cross-Site Scripting (XSS) vulnerability.
- Common pitfalls like accidentally writing secure code or over-complicating projects, and how to troubleshoot them.
By building these demo projects, you’re not just learning about security; you’re becoming an architect of both secure and insecure systems. This hands-on experience is invaluable for truly grasping the nuances of web application security.
What’s Next: In the upcoming chapters, we’ll continue to build upon this foundation, exploring more complex vulnerabilities in our custom labs and delving deeper into advanced exploitation, detection, and robust prevention strategies. Get ready to break what you’ve built, and then make it unbreakable!
References
- Node.js Official Website
- Express.js Official Website
- OWASP Top 10 (2021) - Official Documentation
- OWASP Cheatsheet Series - Cross-Site Scripting Prevention
- OWASP Cheatsheet Series - Access Control
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.