Sample report

A full example review

This is a complete sample report, written against demonstration code that I made up for this page. It shows exactly what a CodeReviewPro report looks like: every finding has a file, a line, the evidence, the impact, a priority and a suggested fix with a time estimate.

Demonstration report — not client work.The code below is fictional sample code, written only to demonstrate the format. No client code, client names or client results appear on this page.

Sample report · 6 findings · 4 critical or high · estimated total fix time 8.5–17 hours

01 / The code

What was reviewed

Four short files from one small Node.js checkout flow: the checkout route itself, the search query it uses, a front-end config file and the mail helper that sends the confirmation. They are complete enough to read top to bottom, and every finding below quotes the lines it points at.

The sample is a checkout route that takes an order, charges a card and lowers the stock, together with the small pieces of surrounding code quoted in the findings. It is deliberately full of the mistakes I find most often in real web shops: stock that is checked but not reserved, a price that comes from the browser, and a payment step with no error handling at all.

src/api/checkout.ts
1import express from 'express';
2import { db, stripe } from '../lib/clients';
3import { sendConfirmation } from '../lib/email';
4
5const app = express();
6
7app.post('/api/checkout', async (req, res) => {
8  const { productId, amount, email } = req.body;
9
10  const item = await db.query(
11    'SELECT * FROM products WHERE id = $1',
12    [productId]
13  );
14  if (!item) return res.status(404).json({ error: 'not found' });
15
16  if (item.stock > 0) {
17    await stripe.charge(amount, email);
18    await db.query(
19      'UPDATE products SET stock = stock - 1',
20      [productId]
21    );
22    const order = await db.query(
23      'INSERT INTO orders (email, total) VALUES ($1, $2) RETURNING id',
24      [email, amount]
25    );
26    await sendConfirmation({ id: order.id, email });
27  }
28
29  res.json({ ok: true });
30});
public/js/config.js
1// public/js/config.js -- downloaded by every visitor
2window.APP_CONFIG = {
3  apiBase: '/api',
4  stripeSecret: 'sk_live_51Hq9xK2eZv…',
5};
src/api/search.ts
1import { db } from '../lib/clients';
2
3export async function searchProducts(q) {
4  const rows = await db.query(
5    `SELECT id, name, price FROM products WHERE name LIKE '%${q}%'`
6  );
7  return rows;
8}
src/lib/email.ts
1import { mailer } from '../lib/clients';
2
3export async function sendConfirmation(order) {
4  const subject = 'Your order ' + order.id;
5  const body = 'Thanks for your order. We ship within two days.';
6  try {
7    await mailer.send(order.email, subject, body);
8  } catch (e) {}
9}
02 / Findings

6 findings, worst first

Ordered by what would cost you the most money or trust.

Critical

Stock is never reserved before payment, so two buyers can buy the same last item

File src/api/checkout.ts  ·  Line 16  ·  Priority Critical — fix before you sell again  ·  Estimated fix time 3–5 hours including tests

Problem

The code reads the stock level, then charges the customer, then lowers the stock. Between the check and the update there is nothing stopping a second request from reading the same stock level and charging a second customer for the same item. The check and the change are two separate steps with a network call in between.

Evidence

16  if (item.stock > 0) {
17    await stripe.charge(amount, email);
18    await db.query(
19      'UPDATE products SET stock = stock - 1',

Impact

Two customers can pay for the same last unit. One of them gets a refund and an apology, the other gets an email they did not expect. On limited or scarce items this happens on launch day, exactly when you cannot afford it. Refunds also cost you payment fees and support time.

Suggested fix

Reserve the item inside a single database transaction before you charge, using row locking (for example SELECT ... FOR UPDATE) or an atomic conditional update that only succeeds while stock is still available. Then charge, then turn the reservation into a real stock reduction. Reserving alone is not a full repair: the failed path has to be handled too — if the payment fails or times out, release the reservation so the item goes back on sale; if the payment succeeds, confirm it once and never twice; give every attempt an idempotency key so a retry cannot charge twice; and return a clear “sold out” response to the customer instead of a silent success. Without those steps you either lose stock or still sell the same item twice.

High

The price comes from the browser, so the customer chooses what to pay

File src/api/checkout.ts  ·  Line 8  ·  Priority High — fix before you sell again  ·  Estimated fix time 1–2 hours

Problem

The amount to charge is taken straight from the request body, and it is passed to the payment call without ever being compared to the price stored in the database. Whatever number the browser sends is the number that gets charged.

Evidence

8  const { productId, amount, email } = req.body;
9
10  const item = await db.query(
11    'SELECT * FROM products WHERE id = $1',
12    [productId]
13  );
14  if (!item) return res.status(404).json({ error: 'not found' });
15
16  if (item.stock > 0) {
17    await stripe.charge(amount, email);

Impact

Anyone can change the amount in their browser or with a simple script and buy a $299 product for $0.01. This is direct revenue loss, and it is easy to find.

Suggested fix

Look the price up on the server from the product id and charge that. Never trust a price, a discount or a total that came from the client. Validate the amount and the currency server-side and log any mismatch between what the client sent and what the database says.

High

SQL injection in the product search

File src/api/search.ts  ·  Line 5  ·  Priority High — fix before you sell again  ·  Estimated fix time 1–3 hours (plus a sweep of the other queries)

Problem

The search term is pasted directly into the SQL string. A quote in the search box breaks out of the string and lets the caller run their own SQL.

Evidence

5    `SELECT id, name, price FROM products WHERE name LIKE '%${q}%'`

Impact

An attacker can read your whole database, change prices, delete rows or dump customer data. This is one of the most exploited bugs on the web and it is usually found automatically by scanners.

Suggested fix

Use parameterised queries or a query builder, so the input can never become part of the SQL statement. The checkout route in this same project already does that for the product lookup; apply the same pattern here: pass the pattern as a bound value instead of building it into the string. Validate and limit the length of the search term as well, and check whether the same pattern appears in other queries in the project.

Critical

A live secret payment key is shipped to the browser

File public/js/config.js  ·  Line 4  ·  Priority Critical — revoke the key today  ·  Estimated fix time 1–2 hours

Problem

A payment secret key is written into a JavaScript file that every visitor downloads. Anything in front-end code is public, no matter how the file is named. A secret key is not a publishable key: it can create charges and read customer and payment data for the whole account.

Evidence

4  stripeSecret: 'sk_live_51Hq9xK2eZv…',

Impact

Anyone who opens the page source can read the key and use it. Unlike a leaked publishable key, this one acts as the account itself: charges can be created on your account, customer and payment records can be read, and the key can be used to issue refunds. Most providers also rotate keys automatically when they detect this pattern, which turns a live shop into a broken one without warning. Treat it as a live credential that is already in the wrong hands.

Suggested fix

Move the key to the server and let your own API talk to the payment provider; the browser should never hold a secret key. Revoke the current key at the provider now and issue a new one, then check the provider dashboard for calls you did not make. If something has to be in the browser, make sure it is the publishable key and not the secret one — and never commit either to a public repository.

Medium

The charge runs without error handling, so a failure after it leaves the customer charged

File src/api/checkout.ts  ·  Line 17  ·  Priority Medium — fix this week  ·  Estimated fix time 2–4 hours

Problem

The charge is not wrapped in a try block: a declined card ends the request with a generic error and no explanation or retry path. The reverse case is the costly one. The charge runs first, so if the stock update or the order insert after it fails, the customer has been charged while no order and no stock change are recorded, and nothing in the code reverses the payment.

Evidence

17    await stripe.charge(amount, email);
18    await db.query(
19      'UPDATE products SET stock = stock - 1',
20      [productId]
21    );
22    const order = await db.query(
23      'INSERT INTO orders (email, total) VALUES ($1, $2) RETURNING id',

Impact

A declined card gives the customer a generic error instead of a clear message, so they retry and can end up paying twice. A customer who was charged with no order to match has to be refunded by hand, after they complain, and the logs hold no list of which payments were affected.

Suggested fix

Wrap the charge in a try block and decide what happens on failure: send a clear message to the customer, leave the stock untouched and log the attempt with enough context to trace it. Write the order row together with the charge so a paid charge always has an order to match, and reconcile the provider afterwards so a payment without a completed order stands out. Confirm payments idempotently, so a repeated callback or a retry cannot charge or confirm the same order twice.

Low

An empty catch block hides failures

File src/lib/email.ts  ·  Line 8  ·  Priority Low — plan it in  ·  Estimated fix time 30–60 minutes

Problem

The order confirmation email is sent inside a try block with an empty catch. If sending fails, nothing is logged and nothing is reported. The checkout route calls this helper on every order, so the failure is invisible on every order.

Evidence

6  try {
7    await mailer.send(order.email, subject, body);
8  } catch (e) {}

Impact

Customers do not get their confirmation and nobody finds out. Support starts asking why people say they never received an email, and there is no trace to debug with.

Suggested fix

Log the error with the order id and enough context to reproduce it, and decide per case whether a failure should be retried, queued or surfaced to the customer. Keep the catch block, just stop it from swallowing the problem.

Total

6 findings in one checkout flow and the files it touches. Estimated repair time together: 8.5–17 hours, of which the 4 critical and high items should be fixed before the shop takes another order.

03 / Reading this report

How to use it

Every finding is written so you can act on it without me.

Evidence, not opinions

Each finding names the file and the line, shows the code that causes the problem, and explains what happens as a result. If a finding cannot be backed up with code, it does not go in the report.

Priorities you can plan with

Critical and high findings are the ones that cost money or trust, and they come first. Medium and low findings are real, but you can schedule them.

Fix estimates per finding

The time estimate is how long the fix takes for a developer who knows the codebase — including a quick test, not including a full rewrite of the feature.

What this does not cover

This report is not a full security audit and it does not guarantee that every problem has been found. Your app is not run or tested live, so problems that only appear under real traffic, on your own servers or in your infrastructure are outside what a code review can see.

Want this for your own code?

Start with a free sample review — up to 3 findings, if identified. No payment required.