openpitstop 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,15 @@ const accounts = [
19
19
  { id: 2, name: "bob", email: "bob@minishop.dev", password: "bob123", isAdmin: true },
20
20
  ];
21
21
 
22
+ // In-memory catalog + order/cart stores so MiniShop feels like a real shop.
23
+ const products = [
24
+ { id: 1, name: "USB-C Cable", price: 9.99, stock: 50 },
25
+ { id: 2, name: "Mechanical Keyboard", price: 79.0, stock: 12 },
26
+ { id: 3, name: "4K Webcam", price: 49.5, stock: 8 },
27
+ ];
28
+ const orders = [];
29
+ const carts = {};
30
+
22
31
  // BUG: command injection — host flows from query string into a shell.
23
32
  app.get("/api/ping", (req, res) => {
24
33
  exec(`ping -c 1 ${req.query.host || "localhost"}`, (err, out) => {
@@ -90,6 +99,47 @@ app.get("/", (req, res) => {
90
99
  res.sendFile(__dirname + "/../views/index.html");
91
100
  });
92
101
 
102
+ // Catalog.
103
+ app.get("/api/products", (req, res) => res.json(products));
104
+ app.get("/api/products/:id", (req, res) => {
105
+ const p = products.find((x) => x.id === Number(req.params.id));
106
+ res.json(p || null);
107
+ });
108
+
109
+ // Cart — no auth, no validation, trusts any body.
110
+ app.post("/api/cart", (req, res) => {
111
+ const { userId, productId, qty } = req.body;
112
+ carts[userId] = carts[userId] || [];
113
+ carts[userId].push({ productId, qty });
114
+ res.json({ cart: carts[userId] });
115
+ });
116
+
117
+ // BUG: price tampering — the server totals the cart from CLIENT-supplied
118
+ // prices, so a shopper can checkout a $79 keyboard for $0.01.
119
+ app.post("/api/checkout", (req, res) => {
120
+ const items = req.body.items || [];
121
+ let total = 0;
122
+ for (const it of items) total += it.price * it.qty; // client controls price
123
+ const order = { id: orders.length + 1, userId: req.body.userId || 1, total, items };
124
+ orders.push(order);
125
+ res.json({ orderId: order.id, total });
126
+ });
127
+
128
+ // BUG: IDOR — any caller can read ANY order by id, no authz/ownership check.
129
+ app.get("/api/orders/:id", (req, res) => {
130
+ const order = orders.find((o) => o.id === Number(req.params.id));
131
+ if (!order) return res.status(404).json({ error: "not found" });
132
+ res.json(order);
133
+ });
134
+
135
+ // BUG: prototype pollution — untrusted body merged via lodash.merge
136
+ // (lodash@4.17.4 is CVE-2019-10744).
137
+ app.patch("/api/profile", (req, res) => {
138
+ const profile = {};
139
+ users.mergeProfile(profile, req.body);
140
+ res.json({ ok: true, profile });
141
+ });
142
+
93
143
  if (require.main === module) {
94
144
  const port = process.env.PORT || 3000;
95
145
  app.listen(port, () => console.log(`minishop listening on ${port}`));
@@ -30,6 +30,15 @@ const accounts = [
30
30
  { id: 2, name: "bob", email: "bob@minishop.dev", password: process.env.DEMO_BOB_HASH || "", isAdmin: true },
31
31
  ];
32
32
 
33
+ // In-memory catalog + order/cart stores (demo stand-ins for the real DB).
34
+ const products = [
35
+ { id: 1, name: "USB-C Cable", price: 9.99, stock: 50 },
36
+ { id: 2, name: "Mechanical Keyboard", price: 79.0, stock: 12 },
37
+ { id: 3, name: "4K Webcam", price: 49.5, stock: 8 },
38
+ ];
39
+ const orders = [];
40
+ const carts = {};
41
+
33
42
  function escapeHtml(s) {
34
43
  return String(s).replace(/[&<>"']/g, (c) =>
35
44
  ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]),
@@ -149,6 +158,58 @@ app.get("/", (req, res) => {
149
158
  res.sendFile(path.join(__dirname, "..", "views", "index.html"));
150
159
  });
151
160
 
161
+ // Catalog.
162
+ app.get("/api/products", (req, res) => res.json(products));
163
+ app.get("/api/products/:id", (req, res) => {
164
+ const p = products.find((x) => x.id === Number(req.params.id));
165
+ res.json(p || null);
166
+ });
167
+
168
+ // FIXED: cart validated; no untrusted bodies trusted blindly.
169
+ app.post("/api/cart", requireAuth, (req, res) => {
170
+ const { userId, productId, qty } = req.body;
171
+ const product = products.find((x) => x.id === Number(productId));
172
+ if (!product) return res.status(400).json({ error: "unknown product" });
173
+ const n = Math.max(0, Math.floor(Number(qty) || 0));
174
+ carts[userId] = carts[userId] || [];
175
+ carts[userId].push({ productId, qty: n });
176
+ res.json({ cart: carts[userId] });
177
+ });
178
+
179
+ // FIXED: total computed from CATALOG prices (server-side), auth required,
180
+ // quantities validated — price tampering is impossible.
181
+ app.post("/api/checkout", requireAuth, (req, res) => {
182
+ const items = req.body.items || [];
183
+ let total = 0;
184
+ for (const it of items) {
185
+ const product = products.find((x) => x.id === Number(it.productId));
186
+ if (!product) return res.status(400).json({ error: "unknown product" });
187
+ const qty = Math.max(0, Math.floor(Number(it.qty) || 0));
188
+ total += product.price * qty;
189
+ }
190
+ const order = { id: orders.length + 1, userId: req.user.sub, total, items };
191
+ orders.push(order);
192
+ res.json({ orderId: order.id, total });
193
+ });
194
+
195
+ // FIXED: ownership check — only the owner (or an admin) can read an order.
196
+ app.get("/api/orders/:id", requireAuth, (req, res) => {
197
+ const order = orders.find((o) => o.id === Number(req.params.id));
198
+ if (!order) return res.status(404).json({ error: "not found" });
199
+ const me = accounts.find((a) => a.id === req.user.sub);
200
+ if (order.userId !== req.user.sub && !me?.isAdmin) {
201
+ return res.status(403).json({ error: "forbidden" });
202
+ }
203
+ res.json(order);
204
+ });
205
+
206
+ // FIXED: prototype-pollution sink removed — only allow-listed fields copied.
207
+ app.patch("/api/profile", requireAuth, (req, res) => {
208
+ const profile = {};
209
+ users.mergeProfile(profile, req.body);
210
+ res.json({ ok: true, profile });
211
+ });
212
+
152
213
  if (require.main === module) {
153
214
  const port = process.env.PORT || 3000;
154
215
  app.listen(port, () => console.log(`minishop listening on ${port}`));