openpitstop 1.5.1 → 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.
- package/README.md +196 -140
- package/demo/DEMO_SCRIPT.md +81 -0
- package/demo/web-app-broken/src/server.js +50 -0
- package/demo/web-app-fixed/src/server.js +61 -0
- package/dist/commands/drive.js +365 -104
- package/dist/commands/drive.js.map +1 -1
- package/dist/commands/next.d.ts +25 -0
- package/dist/commands/next.js +403 -72
- package/dist/commands/next.js.map +1 -1
- package/dist/commands/pen.js +29 -1
- package/dist/commands/pen.js.map +1 -1
- package/dist/graph/correlate.js +11 -1
- package/dist/graph/correlate.js.map +1 -1
- package/dist/pen/drift.d.ts +16 -0
- package/dist/pen/drift.js +69 -0
- package/dist/pen/drift.js.map +1 -0
- package/dist/pen/report.js +90 -2
- package/dist/pen/report.js.map +1 -1
- package/dist/pen/types.d.ts +32 -0
- package/dist/pen/types.js.map +1 -1
- package/package.json +1 -1
- package/templates/pitstop-menu.command.md +26 -21
- package/templates/pitstop.command.md +25 -30
- package/templates/pitstop.prompt.md +18 -0
|
@@ -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
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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}`));
|