config.js:6Possible hardcoded secret: Stripe secret key
stripe…<redacted>: "sk_liv…<redacted>",
Fix: Move to an environment variable / secret store and rotate the exposed value immediately.
This is a real report, shown exactly as a client receives it — every problem named in plain English, with how to fix each one. Leaked secrets are auto-redacted.
config.js:6stripe…<redacted>: "sk_liv…<redacted>",
Fix: Move to an environment variable / secret store and rotate the exposed value immediately.
config.js:6Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data. (rule stripe-access-token).
Fix: Remove the secret from source, rotate it immediately, and load it from an environment variable / secret store.
config.js:8Detected a Generic API Key, potentially exposing access to various services and sensitive operations. (rule generic-api-key).
Fix: Remove the secret from source, rotate it immediately, and load it from an environment variable / secret store.
db.js:7db.js:7 uses string interpolation directly in a prepared statement string: `SELECT * FROM bookings WHERE id = '${id}'`. An attacker can pass id=1' OR '1'='1 to dump all bookings or perform destructive operations.
Fix: Use parameterized queries: db.prepare('SELECT * FROM bookings WHERE id = ?').get(id)
db.js:12db.js:12 concatenates user-supplied `name` directly into a SQL string passed to db.exec(). This allows full SQL injection including UNION attacks and data exfiltration from the admin endpoint.
Fix: Use db.prepare('SELECT * FROM bookings WHERE customer_name LIKE ?').all('%' + name + '%')
db.js:17db.js:17 interpolates all four user-supplied fields (name, phone, service, slot) directly into a SQL string. Any field can break out of the string literal and execute arbitrary SQL.
Fix: Use a parameterized INSERT with db.prepare(...).run(b.name, b.phone, b.service, b.slot)
config.js:6config.js:6 contains a deliberately seeded Stripe-style secret key (sk_live_...) committed in plaintext. Anyone with repo access can make charges, issue refunds, or access customer payment data.
Fix: Immediately rotate the key in Stripe dashboard. Load all secrets from environment variables or a secrets manager; never commit them.
config.js:7config.js:7 contains a static, weak JWT signing secret. Any token signed with this key can be forged by anyone who reads the source.
Fix: Generate a cryptographically random secret (≥256 bits) and load it from an environment variable.
config.js:8config.js:8 contains a live SendGrid API key in plaintext, enabling an attacker to send arbitrary email as the application's account.
Fix: Rotate the key immediately and load from environment variable.
server.js:22server.js:22-24 exposes GET /api/admin/bookings with no token or session check. Any unauthenticated user can retrieve all bookings in the system.
Fix: Add an authentication middleware that validates a JWT or session token before allowing access to /api/admin/* routes.
server.js:10server.js:10-13 sets Access-Control-Allow-Origin: * AND Access-Control-Allow-Credentials: true. Browsers block credentialed requests to wildcard origins per spec, but this signals intent to allow cross-origin credential sharing and is a misconfiguration that could be exploited if the origin is later narrowed incorrectly.
Fix: Set a specific allowed origin whitelist and only echo back origins that are explicitly permitted. Remove the wildcard.
config.js:5config.js:5 contains a plaintext production database password. This is committed to source control and visible to anyone with repo access.
Fix: Remove from source, rotate the credential, and load from environment variable.
db.js:12db.js:12 and db.js:17 use db.exec() which executes SQL but returns a statement result object, not rows. For searchBookings this means the admin console always returns an unusable object instead of booking records.
Fix: Use db.prepare(...).all() for SELECT and db.prepare(...).run() for INSERT/UPDATE/DELETE.
bookings.js:9bookings.js:9-12 extracts only the hour from a slot ISO string. Two bookings on different days at the same hour will be treated as conflicting, and two bookings in the same hour on the same day at :00 and :30 will also collide — the logic is wrong in both directions.
Fix: Compare full ISO slot strings (or Unix timestamps) for exact equality. Also, slotFree is never called from createBooking, so it has no effect currently.
db.js:16createBooking in db.js does not call slotFree, so double-bookings are never prevented regardless of the slot-checking logic.
Fix: Call slotFree (after fixing it) inside createBooking before inserting, and return an error if the slot is taken.
server.js:16server.js and db.js accept all fields from req.body/req.query without type, length, or format checks. Malformed data will either cause SQL errors or be stored as garbage.
Fix: Add a validation layer (e.g., zod, joi, or express-validator) for all incoming request fields.
auth.js:5auth.js:5 checks username === 'admin' && password === 'admin123'. These are trivially guessable default credentials with no rate limiting or lockout.
Fix: Store hashed passwords (bcrypt/argon2) in the database. Add rate limiting and account lockout.
auth.js:11auth.js:11 generates session tokens using Math.random() which is not cryptographically secure, making tokens predictable/guessable.
Fix: Use crypto.randomBytes(32).toString('hex') for session tokens.
public/app.js:5public/app.js:5-6 sets innerHTML using b.customer_name, b.service, and b.slot directly from the API response. A stored XSS payload in any booking field will execute in the admin's browser.
Fix: Use textContent or a sanitization library (DOMPurify). Never assign untrusted data to innerHTML.
public/app.js:10public/app.js:10 reads the 'staff' query parameter and assigns it to innerHTML without any sanitization, allowing reflected XSS via a crafted URL.
Fix: Use textContent instead of innerHTML, or sanitize the value before insertion.
server.js:17server.js:17 exposes booking details to any caller with a booking ID. There is no token check, so any unauthenticated user who guesses or enumerates an ID can read customer PII.
Fix: Require authentication or at minimum a signed token tied to the specific booking.
server.js:18server.js:18 allows anyone to create bookings without authentication, enabling spam/abuse.
Fix: Add rate limiting and, for admin-created bookings, require authentication.
auth.js:5auth.js returns { ok: true, token: ... } but there is no middleware that stores or validates this token on subsequent requests. The token is issued but never checked anywhere.
Fix: Implement token validation middleware and apply it to protected routes.
db.js:3db.js opens bookwell.db but never creates the bookings table. On a fresh install all queries will throw 'no such table: bookings'.
Fix: Add a schema initialization block (db.exec('CREATE TABLE IF NOT EXISTS bookings (...)')) or a migration tool.
server.js:16server.js routes call db/auth functions with no try/catch. A bad query or missing field will throw an unhandled exception, crashing the Express process.
Fix: Wrap route handlers in try/catch and return appropriate HTTP error responses.
config.js:4config.js:4 has '// TODO: move these before go-live' but the app appears to be using live keys already (sk_live_ prefix on Stripe key), indicating this was never done.
Fix: Treat this as a critical security issue, not a completeness note. Rotate all secrets immediately.
server.jsThere is a login endpoint but no logout. Sessions (such as they are) cannot be invalidated.
Fix: Implement a POST /api/logout endpoint that invalidates the session token.
db.jsThe bookings table is referenced throughout but never defined. There is no migration file, no CREATE TABLE statement, and no seed data.
Fix: Add a schema.sql or inline initialization in db.js.
bookings.js:10bookings.js:10 runs SELECT slot FROM bookings with no WHERE clause, loading all historical bookings into memory to check a single slot.
Fix: Query for the specific slot: SELECT 1 FROM bookings WHERE slot = ? LIMIT 1
server.js:16All route handlers call res.json() without setting a status code. Errors, not-found results, and auth failures all return HTTP 200, making client-side error handling impossible.
Fix: Use res.status(404).json(...), res.status(401).json(...), etc. as appropriate.
server.js:16POST /api/login has no rate limiting, allowing unlimited brute-force attempts against the hardcoded credentials.
Fix: Add express-rate-limit or similar middleware to /api/login.
config.js:6Stripe API Key detected
Fix: Stripe API Key detected
server.js:6A CSRF middleware was not detected in your express application. Ensure you are either using one such as `csurf` or `csrf` (see rule references) and/or you are properly doing CSRF validation in your routes with a token or cookies.
Fix: A CSRF middleware was not detected in your express application. Ensure you are either using one such as `csurf` or `csrf` (see rule references) and/or you are properly doing CSRF validation in your routes with a token or cookies.
server.js:18State-changing POST endpoints have no CSRF token requirement. Combined with the overly permissive CORS policy this increases CSRF risk.
Fix: Add csurf middleware or use SameSite cookie attributes and verify Origin headers.
server.jsThe app sets no Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, or other security headers, leaving the admin console vulnerable to clickjacking and MIME sniffing.
Fix: Add the helmet middleware: app.use(require('helmet')())
config.js:4// TODO: move these before go-live
Fix: Finish or remove this before launch.
config.js:6config.js stores a Stripe secret key but no Stripe SDK is in package.json and no payment code exists. The key is exposed for no current benefit.
Fix: Remove the key until payment functionality is actually implemented; load from env when needed.
config.js:8config.js:8 has a SendGrid API key but there is no email-sending code anywhere in the codebase.
Fix: Remove until implemented; load from environment variable when needed.
server.jsThe API only supports create and read. There is no way to cancel, reschedule, or delete a booking through the API.
Fix: Implement PUT /api/booking/:id and DELETE /api/booking/:id as needed.
server.js:25server.js:25 logs the port on startup via console.log. While minor, it confirms the deterministic signal about debug output.
Fix: Use a structured logger (pino, winston) with log levels rather than bare console.log.
auth.js:14auth.js:14 exports newSessionToken unnecessarily, exposing an internal implementation detail.
Fix: Remove newSessionToken from exports; it is an internal helper.
package.jsonpackage.json has no jsonwebtoken or jose dependency, yet config.js defines a jwtSecret. Either JWT is not implemented or the dependency is missing.
Fix: Either implement JWT auth with a proper library or remove the unused secret.
server.js:25app.listen(config.port, () => console.log("BookWell on " + config.port));
Fix: Finish or remove this before launch.
config.js:5config.js:5 defines dbPassword but better-sqlite3 does not support password-protected databases natively. This field is misleading and unused.
Fix: Remove or clarify; if SQLCipher is intended, document and implement it explicitly.
See real before/after receipts — every named defect re-checked and closed — on the case studies.
Get your scan →