OWASP Juice Shop’s SQL injection and cross-site scripting (XSS) challenges are the foundation of web application security practice. They teach the two most common and well-documented vulnerability classes in a realistic Angular + Node.js + SQLite environment — the same technology stack found in modern e-commerce and SaaS applications.
This guide covers every SQL injection and XSS challenge in Juice Shop with exact payloads, technical explanations of why they work, and what each challenge teaches about real-world vulnerabilities.
Start Juice Shop with:
docker run --rm -p 3000:3000 bkimminich/juice-shop
Then open http://localhost:3000/#/score-board to track your progress.
Part 1: SQL Injection Challenges
SQL injection in Juice Shop targets its SQLite database through the Node.js backend. The application uses Sequelize ORM but has intentional raw query vulnerabilities on specific endpoints — exactly the pattern found in real codebases where developers mix safe ORM usage with unsafe raw queries for “performance” or “flexibility.”
Challenge: Login Admin (⭐⭐)
Objective: Log in as the administrator without knowing the password.
Payload:
- Email field:
' OR '1'='1'-- - Password field: anything (e.g.,
x)
Step-by-step:
- Go to
http://localhost:3000/#/login - Enter
' OR '1'='1'--in the Email field - Enter any value in the Password field
- Click Log In
What the payload does:
The login query is constructed as:
SELECT * FROM Users WHERE email = '<EMAIL>' AND password = '<PASSWORD_HASH>'
With the injection payload, it becomes:
SELECT * FROM Users WHERE email = '' OR '1'='1'-- AND password = '...'
Breaking this down:
'' OR '1'='1'— the WHERE clause now returns TRUE for every row (because'1'='1'is always true)--— comments out the rest of the query (the password hash check)- The query returns ALL users; the application logs in as the first returned user — which is the admin
Why this works: The login endpoint constructs a SQL query with string concatenation. There is no parameterization, so the single quote terminates the email string literal and the injected SQL is interpreted by the SQLite engine.
Real-world analog: This identical vulnerability pattern exists in legacy PHP login forms using mysql_query() or mysqli_query() with concatenated strings — still present in older e-commerce platforms, CMS installations, and enterprise applications with legacy codebases.
The fix:
// VULNERABLE — string concatenation
const query = `SELECT * FROM Users WHERE email = '${email}' AND password = '${hash}'`;
// SECURE — parameterized query
const query = `SELECT * FROM Users WHERE email = ? AND password = ?`;
db.query(query, [email, hash]);
Challenge: Login Jim (⭐⭐)
Objective: Log in with Jim’s user account (without knowing his password).
Step 1: Find Jim’s email
Jim has left product reviews. Navigate to any product page and look for reviews from “Jim.” His email is [email protected].
Alternatively, if the /api/Users endpoint is accessible (it may be after gaining admin access), Jim’s account appears there.
Step 2: Use SQL injection to bypass Jim’s password
- Email field:
[email protected]'-- - Password field: anything
What happens:
-- The query becomes:
SELECT * FROM Users WHERE email = '[email protected]'-- AND password = '...'
The ' closes the email string. -- comments out the password check. The query returns Jim’s account directly.
What this teaches: SQL injection for targeted account takeover — not just “log in as anyone” but “log in as a specific known user.” This is the more dangerous real-world variant: attackers typically target specific high-value accounts (administrators, finance accounts) rather than using generic bypass payloads.
Challenge: Login Amy (⭐⭐⭐)
Objective: Log in with Amy’s user account using SQL injection.
Step 1: Find Amy’s email
Amy’s email ([email protected]) can be discovered through:
- The admin panel’s user list (accessible after the Login Admin challenge)
- The
/api/Usersendpoint with admin authentication
Step 2: Time-based SQL injection (if direct approach is blocked)
For this challenge, direct SQL injection may not work on all Juice Shop versions. An alternative approach uses the forgot password flow:
- Navigate to
http://localhost:3000/#/forgot-password - Enter
[email protected] - The security question is “Mother’s maiden name?” — Amy’s maiden name is “Walton” (discoverable through LinkedIn-style clues on the user profile)
- Answer:
Walton - Reset the password and log in
What this teaches: Authentication bypass through weak security questions — a non-SQL-injection path to account compromise. Real-world security questions like “mother’s maiden name” or “first pet” are often publicly discoverable through social media, making them effectively public knowledge for targeted attacks.
Challenge: Database Schema (⭐⭐⭐)
Objective: Exfiltrate the entire database schema using SQL injection.
Vulnerable endpoint: The product search: http://localhost:3000/rest/products/search?q=
Payload (UNION-based injection):
http://localhost:3000/rest/products/search?q=')) UNION SELECT sql, 2, 3, 4, 5, 6, 7, 8, 9 FROM sqlite_master--
Breakdown:
'))closes the search string and the parenthesis in the SQL queryUNION SELECTappends the results of a second query to the original resultssql, 2, 3, 4, 5, 6, 7, 8, 9selects the SQL schema (column 1) and dummy values for the other 8 columns (the product table has 9 columns)FROM sqlite_master— SQLite’s system table that stores all CREATE TABLE statements
The response includes the CREATE TABLE statements for all database tables — revealing every column name, data type, and relationship.
What this teaches: UNION-based SQL injection for database enumeration. This is the technique attackers use after gaining initial SQL injection to understand the full database schema and plan targeted data extraction. In real assessments, this leads to identifying where PII, credentials, and payment data are stored.
How to find the column count: Before the UNION attack works, you need to know the number of columns in the original query. The technique:
# Try increasing column counts until no error:
')) UNION SELECT 1--
')) UNION SELECT 1,2--
# ... continue until success
')) UNION SELECT 1,2,3,4,5,6,7,8,9-- # 9 columns - works for Juice Shop
Challenge: User Credentials (⭐⭐⭐⭐)
Objective: Retrieve all user email addresses and password hashes from the database using SQL injection.
Payload (after discovering the schema):
http://localhost:3000/rest/products/search?q=')) UNION SELECT id, email, password, 4, 5, 6, 7, 8, 9 FROM Users--
This UNION injects a query against the Users table, pulling user IDs, emails, and password hashes into the product search results.
What you get: A list of all user accounts with their email addresses and bcrypt password hashes. While bcrypt hashes cannot be reversed easily, several Juice Shop user accounts use weak passwords that appear in common wordlists.
What this teaches: Complete credential exfiltration via SQL injection. This is the end goal of most SQL injection attacks in real applications — extracting the user table to use offline for cracking or credential stuffing attacks.
Part 2: Cross-Site Scripting (XSS) Challenges
Juice Shop contains all three XSS types: reflected (fires when a victim loads a crafted URL), stored (fires for every user who views the infected page), and DOM-based (fires when client-side JavaScript processes the URL without sanitization). Each type has a different attack vector and different real-world impact.
Challenge: DOM XSS (⭐)
Objective: Perform a DOM XSS attack using <iframe src="javascript:alert('xss')">.
Payload:
- Go to
http://localhost:3000/ - In the search bar, type:
<iframe src="javascript:alert('xss')"> - Press Enter
What happens:
The Angular application renders the search term in the DOM without sufficient sanitization. The <iframe> element is inserted into the DOM and the javascript: URL executes in the iframe’s src attribute context. An alert popup appears, and the challenge registers as solved.
Why <iframe src="javascript:..."> instead of <script>?
Angular’s built-in sanitization strips <script> tags but historically allowed certain javascript: protocol usages in attribute contexts. The <iframe src="javascript:alert('xss')"> payload exploits the difference between content sanitization (which strips script tags) and attribute-level sanitization (which may allow specific pseudo-protocol usages).
Technical detail:
The vulnerable Angular template renders the search term using a binding that doesn’t use Angular’s strict trust model:
// Simplified vulnerable pattern
this.searchTerm = userInput; // No explicit sanitization
// Template: <div [innerHTML]="searchTerm"></div> — innerHTML binding bypasses some protections
Real-world analog: DOM XSS in Angular applications occurs when developers use [innerHTML] or DomSanitizer.bypassSecurityTrustHtml() incorrectly. This is a common mistake when developers need to render “formatted” user content (like product descriptions with basic HTML) without understanding the XSS implications.
The fix:
// VULNERABLE — bypasses Angular's security
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(userInput);
// SECURE — use Angular's text rendering (treats input as text, not HTML)
this.displayText = userInput; // In template: {{ displayText }} — always text-safe
// SECURE — if HTML rendering is necessary, use DOMParser with allowlist
Challenge: Reflected XSS (⭐⭐)
Objective: Perform a reflected XSS attack.
Vulnerable endpoint: The order tracking page.
Payload:
http://localhost:3000/#/track-result?id=<iframe src="javascript:alert('xss')">
Navigate to that URL and the iframe XSS fires.
How this works:
The order tracking page takes the id URL parameter and renders it in the page — without encoding — as part of the order status display. The Angular router processes the URL parameter and passes it to a component that renders it unsafely.
Technical explanation:
// Vulnerable Angular component
ngOnInit() {
this.orderId = this.route.snapshot.queryParams['id'];
// Template: <span [innerHTML]="orderId"></span> — renders as HTML, not text
}
The [innerHTML] binding renders the parameter as HTML. The <iframe src="javascript:alert('xss')"> payload creates an iframe element that executes the JavaScript URL.
Real-world analog: Reflected XSS in URL parameters is one of the most common XSS classes in real applications. Tracking links, confirmation pages, and error messages that display user-controlled URL parameters are frequent sources. Phishing attacks deliver reflected XSS via crafted URLs — the victim clicks a legitimate-seeming link to a trusted domain, but the URL contains the XSS payload.
Impact: Reflected XSS allows:
- Session cookie theft (if
HttpOnlyis not set) - Keylogging on the vulnerable page
- Phishing overlays on the victim’s view of the application
- Cross-site request forgery that bypasses CSRF protections
Challenge: API-Only XSS (⭐⭐⭐)
Objective: Perform a persisted XSS attack that bypasses the application’s client-side sanitization by targeting an API endpoint directly.
Approach:
Juice Shop’s user profile has a “Website” field. Submitting a JavaScript URL through the frontend may be filtered. However, the API accepts the value directly:
- Log in as any user
- Intercept the profile update request via browser DevTools → Network
- The API call is
PUT /api/Users/6(or your user ID) with a JSON body includingwebsite - Send the request directly to the API with a crafted payload:
curl -X PUT http://localhost:3000/api/Users/6 \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"website": "javascript:alert(\"xss\")"}'
- Navigate to your profile page — clicking the website link executes the JavaScript
What this teaches: Client-side validation bypass via direct API access. The frontend may validate and sanitize input before submitting it, but the API endpoint accepts any value. Attackers routinely bypass client-side validation by interacting directly with the API — using curl, Postman, Burp Suite, or browser DevTools to craft requests the frontend would never send.
Challenge: Bonus Payload (⭐⭐)
Objective: Use the alert(document.domain) XSS payload to complete the challenge.
Payload variations:
<!-- In search bar: -->
<iframe src="javascript:alert(document.domain)">
<!-- Or using event handlers: -->
<img src=x onerror="alert(document.domain)">
<!-- Or using SVG: -->
<svg onload="alert(document.domain)">
Why document.domain matters:
alert(document.domain) is the canonical XSS proof-of-concept payload used in security assessments and bug bounty reports — it demonstrates that JavaScript is executing in the context of the target domain (localhost:3000 in Juice Shop’s case).
This is meaningful because:
- It proves the script has access to the page’s same-origin context (cookies, DOM, localStorage)
- It differentiates from scenarios where a script executes in a sandboxed
nullorigin (where it would shownullinstead of the domain) - It’s the standard payload that security researchers use to confirm an XSS finding before reporting it
In bug bounty reports: A submission that includes alert(document.domain) firing on the target domain is unambiguous proof of XSS exploitability. alert(1) can sometimes be confused with self-XSS or non-exploitable behavior, but document.domain confirms full same-origin access.
Challenge: Stored XSS via Product Reviews (⭐⭐⭐)
Objective: Store an XSS payload in a product review that executes when other users view the product.
Steps:
- Navigate to any product page
- Click to write a review
- In the review text, enter:
<iframe src="javascript:alert('xss')"> - Submit the review
- Navigate to the product page as any other user — the XSS fires when the review is rendered
Technical explanation:
The product review is stored in the database as entered. When any user views the product page, the review is retrieved and rendered. If the review text is rendered with [innerHTML] or equivalent:
// Vulnerable rendering
review.text = storedReviewText; // Contains the XSS payload
// Template: <div [innerHTML]="review.text"></div> — renders and executes
Why stored XSS is more dangerous than reflected:
Reflected XSS requires tricking the victim into clicking a crafted URL — the payload is in the URL and only fires for users who receive and click the malicious link.
Stored (persistent) XSS fires automatically for every user who views the page. No crafted URL is needed — the malicious script is embedded in the application’s own database and served to all users by the application itself.
Real-world stored XSS targets: Comment sections, product reviews, user bios, ticket descriptions, forum posts, and any other user-generated content that is stored and then displayed to other users.
Real-world impact: Stored XSS in a high-traffic e-commerce application can:
- Steal session cookies from thousands of users simultaneously
- Redirect all visitors to a phishing page
- Execute browser cryptomining scripts on every visitor’s machine
- Silently perform CSRF attacks on behalf of every victim who views the page
Challenge: Persistent XSS — Admin User Login (⭐⭐⭐)
Objective: Perform a stored XSS attack that executes when the admin next logs into the admin panel.
Approach:
- In the username field during registration, create an account with a name containing an XSS payload:
Username: <iframe src="javascript:alert('xss')">
- When the admin views the user management list at
/#/administration, the XSS fires
Alternative approach via last login IP:
The admin panel displays the last login IP address for each user. If the application renders IP addresses from the database as HTML without encoding, an attacker who can control what IP address gets stored can inject a payload.
Some Juice Shop configurations allow manipulating the lastLoginIp via the profile API:
curl -X PUT http://localhost:3000/api/Users/YOUR_ID \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"lastLoginIp": "<iframe src=\"javascript:alert(xss)\">"}'
What this teaches: Stored XSS in administrative interfaces. XSS payloads stored in fields visible only to admins (usernames, IP addresses, audit log entries) are particularly high-value — they execute in the admin’s session, potentially giving the attacker admin-level access to the application.
Part 3: Chaining SQL Injection and XSS
The most sophisticated Juice Shop challenges involve chaining multiple vulnerabilities. Understanding how SQL injection and XSS interact is critical for real-world assessments.
SQLi → Account Takeover → Stored XSS Delivery
A real attack chain might look like:
- SQL injection in login → gain access to the admin account
- Admin panel access → view all user data and edit user profiles
- Edit a user’s profile → inject a stored XSS payload into a field displayed to other users
- Other users view the infected page → the XSS fires in their browser sessions
- XSS payload → steals session tokens and sends them to an attacker-controlled server
This attack chain transforms a SQL injection vulnerability into a persistent cross-site scripting attack that compromises all users.
SQLi → Password Hash Extraction → Credential Stuffing
- UNION-based SQL injection → exfiltrate the Users table (emails + password hashes)
- Offline hash cracking → crack weak bcrypt hashes against common password wordlists
- Credential stuffing → use cracked email/password pairs to log into other services (users reuse passwords)
Using These Challenges to Benchmark DAST Scanners
OWASP Juice Shop’s SQL injection and XSS challenges serve as the minimum benchmark for DAST scanner validation. Before deploying any DAST tool against your production application, verify that it automatically detects:
| Vulnerability | Juice Shop Location | Expected Scanner Finding |
|---|---|---|
| SQL injection | Login form (/rest/user/login) | Critical |
| SQL injection | Search endpoint (/rest/products/search) | Critical |
| Reflected XSS | Search bar | High |
| Reflected XSS | Order tracking URL parameter | High |
| Stored XSS | Product reviews | High |
| DOM-based XSS | Angular template rendering | Medium |
A DAST scanner that misses the SQL injection in Juice Shop’s login form — the most obvious, unobfuscated SQL injection test case in any application — is not ready for production use.
Offensive360 DAST is verified against these benchmarks on every release. To test your web application for the same vulnerability classes found in Juice Shop:
- Book a demo — see an authenticated DAST scan run live against a real application
- Book a demo — see live scanning against Juice Shop or your own application
Detecting SQL Injection and XSS with SAST
Beyond dynamic testing, SAST (Static Application Security Testing) can detect SQL injection and XSS vulnerabilities in source code before deployment — catching them at the code review stage rather than in a running application.
SAST Detection of SQL Injection
Offensive360 SAST detects SQL injection in Node.js/Express code by tracing user-controlled data from HTTP request parameters through to db.query(), sequelize.query(), or similar sink methods without parameterization:
// Source: req.query.q (user-controlled input)
const searchTerm = req.query.q;
// Sink: sequelize.query() with string interpolation — SQLi detected
const result = await sequelize.query(
`SELECT * FROM Products WHERE name LIKE '%${searchTerm}%'`
);
The SAST engine traces the data flow from req.query.q to the sequelize.query() call and flags the interpolation as a confirmed SQL injection finding.
SAST Detection of XSS
Stored XSS is detected by tracing:
- User input → storage (INSERT to database)
- Database retrieval → template rendering without encoding
// Stored XSS taint flow:
const reviewText = req.body.text; // Source: user input
await Review.create({ text: reviewText }); // Stored to DB
// Later, in a different route:
const reviews = await Review.findAll();
res.render('product', { reviews }); // Passed to template
// In template (EJS example):
// <%- review.text %> ← unescaped (vulnerable) vs <%= review.text %> (escaped, safe)
The SAST engine identifies the unescaped template output (<%-) as an XSS sink when the data source is user-controlled.
Frequently Asked Questions
What SQL injection payload bypasses Juice Shop’s login?
The canonical login bypass payload for Juice Shop is: ' OR '1'='1'-- in the email field (with any value in the password field). This exploits the unparameterized SQLite query in the login endpoint. See the Login Admin challenge above for the full explanation.
Why does alert(document.domain) work in Juice Shop?
Juice Shop’s Angular application renders certain user-supplied strings using [innerHTML] binding or in contexts where javascript: pseudo-protocol URLs are executed. alert(document.domain) confirms the XSS payload is executing in the localhost:3000 origin context. See the Bonus Payload challenge above.
Is the SQL injection in Juice Shop exploitable by automated DAST scanners?
Yes — any production-ready DAST scanner should detect the SQL injection in Juice Shop’s login form and product search endpoint. These are straightforward, unobfuscated injection points using classic ' OR '1'='1'-- patterns. Automated scanners should also detect reflected XSS in the search bar. If your scanner misses these findings, it requires tuning before use on production applications.
What is the difference between Juice Shop’s SQL injection and real-world SQL injection?
Juice Shop’s SQL injection is deliberately obvious — the vulnerable endpoints use string concatenation without any attempt at sanitization. In real-world applications, SQL injection is more subtle:
- Partial sanitization that misses certain characters
- Injection in less obvious inputs (HTTP headers, JSON body fields, file names)
- Second-order injection where the payload is stored and triggers in a different context
- Blind injection where there is no direct output (time-based or boolean-based detection required)
Juice Shop’s examples teach the fundamental mechanism. The variants above teach the real-world complexity.
Can I practice Juice Shop SQL injection and XSS online?
The official Juice Shop demo server (URL at github.com/juice-shop/juice-shop) provides a shared instance. For real practice, a local Docker instance is far better — you have full control, private access, and can run any tools against it.
Summary
OWASP Juice Shop’s SQL injection and XSS challenges cover the most important and commonly exploited vulnerability classes in web application security:
| Challenge | Stars | Technique | Real-World Pattern |
|---|---|---|---|
| Login Admin | ⭐⭐ | SQLi authentication bypass | Legacy login forms, unparameterized queries |
| Login Jim | ⭐⭐ | Targeted SQLi account takeover | Targeted credential theft |
| Database Schema | ⭐⭐⭐ | UNION-based SQLi exfiltration | Database structure enumeration |
| User Credentials | ⭐⭐⭐⭐ | Full credential extraction | Password hash exfiltration |
| DOM XSS | ⭐ | javascript: in attribute context | Angular [innerHTML] misuse |
| Reflected XSS | ⭐⭐ | URL parameter rendering | Phishing via crafted links |
| Stored XSS | ⭐⭐⭐ | Persistent payload in reviews | User-generated content vectors |
| Admin-targeted XSS | ⭐⭐⭐ | Admin panel payload delivery | High-privilege XSS escalation |
alert(document.domain) | ⭐⭐ | PoC validation payload | Bug bounty confirmation technique |
For the complete challenge list, setup instructions, and DAST benchmarking guidance, see our OWASP Juice Shop guide.
Scan your web application for SQL injection, XSS, and the full OWASP Top 10 with Offensive360 DAST. Book a demo to see an authenticated scan of a live application. Or see Offensive360 SAST for source-code-level detection of these vulnerability classes in your codebase.