A quote in a search term exposed a problem in the original Atlas Market account: the endpoint assembled SQL from user-controlled text.

The remediation replaced that construction with a parameterized statement. But the edited query also added LIMIT 50. Those are two changes with different questions to answer: does the input remain data, and does the new result cap preserve what callers need?

The excerpts below show both changes. The test results are from the original write-up, not a new run against the current application.

01 / FIELD NOTESThe input was part of the statement

The vulnerable excerpt builds its search query with a template literal. The search term becomes part of the SQL string handed to db.query.

listings.js · vulnerable source / JavaScript
// search endpoint — DO NOT ship this
app.get("/api/listings", (req, res) => {
  const q = req.query.q;
  const sql = `SELECT * FROM listings WHERE title LIKE '%${q}%'`;
  db.query(sql).then(rows => res.json(rows));
});
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

This is the vulnerable historical example, not code to deploy. It shows the pattern described in the source; no request is sent when you read or interact with this page.

02 / FIELD NOTESReproduce the behavior before changing it

The original account says a failing regression was written before the patch. The published test sends the attack string through the endpoint and checks the response status and the table's continued existence.

listings.test.js · source regression / JavaScript
it("rejects SQL control characters in search", async () => {
  const res = await request(app)
    .get("/api/listings")
    .query({ q: "'; DROP TABLE listings;--" });
  expect(res.status).toBe(200);
  await expect(tableExists("listings")).resolves.toBe(true);
});
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

The test name says it “rejects SQL control characters,” but the assertions expect a successful response and an existing table. The code does not assert that the input was rejected. The distinction is in the assertions, regardless of the test's name.

These are destructive-payload test materials from the original article. They belong in an isolated test environment with disposable data, not a live database.

03 / FIELD NOTESKeep the statement and value separate

The revised excerpt passes a fixed statement and a values array to db.query:

listings.js · parameterized source / JavaScript
app.get("/api/listings", (req, res) => {
  const q = `%${req.query.q ?? ''}%`;
  db.query(
    'SELECT * FROM listings WHERE title LIKE $1 LIMIT 50',
    [q],
  ).then(rows => res.json(rows));
});
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

The search term is wrapped in a LIKE pattern and supplied as the value for $1. The interactive explanation below shows the distinction without executing SQL.

SQL / STATEMENT + VALUES

Let the query stay a query.

Type a search term to compare string assembly with a parameterized statement. This never runs SQL.

BEFORE / DO NOT DEPLOY

Input inside the SQL text

SELECT * FROM listings WHERE title LIKE '%O'Brien%'

The statement itself changes as the term changes.

AFTER / PUBLISHED PATTERN

Statement and value, separate

SELECT * FROM listings WHERE title LIKE $1 LIMIT 50
$1["%O'Brien%"]

The statement stays constant. The value carries the term.

Rendering strings only. Nothing is sent, executed, or saved.

A local explanation of the two source excerpts. It is not a SQL parser, an escaping library, or proof that the full endpoint is secure.

Changing the example term changes the bound value, not the statement. This is not a substitute for reviewing the actual driver, query, and calling code.

04 / FIELD NOTESLIMIT 50 is a separate compatibility question

The original article says callers were unchanged and describes the patch as having zero breaking changes. The excerpts establish the same route and broadly similar response handling. They also show a new maximum result set.

COMPATIBILITY / CHECK THE WHOLE CHANGE

Same route does not mean identical behavior.

The fixed excerpt also adds LIMIT 50. That changes the maximum result set.

BEFORE / NO LIMIT IN EXCERPT61possible matching rows
AFTER / LIMIT 5050maximum returned here

11 illustrative matching records fall beyond the new cap. Caller compatibility needs a separate check.

Illustrative counts—not Atlas traffic, customer data, or a database measurement. The source does not include callers or pagination tests that would establish compatibility.

The source does not supply caller tests or pagination behavior that would resolve that difference. A caller that previously relied on receiving more than fifty matches may need attention. Compatibility therefore remains a question for the calling code, not a conclusion established by this test.

That does not make the result limit inherently wrong. It makes it another behavior to review and test, rather than an invisible detail of the injection fix.

05 / FIELD NOTESReported outcome, with the source limits intact

The original write-up reports that the failing regression passed after the change, two additional endpoints were found and fixed, and a lint rule was added to flag raw query construction. Those outcomes have not been independently rerun here.

There is also a framework mismatch to resolve: these snippets use Express-style (req, res) handlers, while the Atlas architecture article describes a Hono backend mounted inside Next.js. The supplied revisions do not establish how the accounts fit together. The excerpts stay attributed to their source instead of being silently ported to another framework.

06 / FIELD NOTESWhat the original next steps were

The original article proposed least-privilege database roles, query timeouts, and centralized logging on the search path as further work. The source does not establish that those changes were subsequently implemented.

The enduring lesson in this case is not a claim that one patch made the whole application secure. It is to keep the evidence attached to each claim: the vulnerable construction, the test's actual assertions, the parameterized replacement, and the compatibility questions left by the rest of the diff.

Read why I test the attack before the fix, or return to the Atlas project.

Source, dates, and what this page verifies

This rewrite is grounded in the supplied project account and complete related articles. Publication dates describe those accounts, not a new release. The original application, its permissions, screenshots, test results, and deployment have not been re-audited.

The diagrams are local explanations with no API calls, database operations, test execution, or saved input. Source conflicts and unsupported outcomes are identified in the reading copy and the handoff. No production deployment occurred.

Open the original published source ↗