Q3 of 24 · Security

What is SQL injection and how does a QA tester verify that inputs are protected?

SecurityJuniorsecuritysql-injectioninjectionowasptestingfundamentals

Short answer

Short answer: SQL injection occurs when user input is interpolated into a SQL query, allowing an attacker to alter the query's logic. A QA tester validates protection by submitting injection payloads in text fields and URL parameters, then asserting the response is a normal error or result — not leaked data, not an unhandled exception, not an HTTP 500.

Detail

The vulnerability occurs when code builds a SQL query with string concatenation rather than parameterised queries (prepared statements). A payload like ' OR '1'='1 injected into a login field can turn WHERE username = 'input' into WHERE username = '' OR '1'='1', which evaluates to true for every row — bypassing authentication entirely.

What a QA tester tests:

  • Submit common injection payloads in every user-controlled text field: ', ' OR '1'='1' --, '; DROP TABLE users; --, 1 UNION SELECT null--
  • Submit payloads in URL query parameters, form fields, headers (User-Agent, Referer), and JSON body fields in API requests
  • Assert the expected response: a validation error ("invalid input") or a normal business-logic error. A correct response is not a 500, not a database error message, not a stack trace.
  • Assert the application does NOT return data from tables other than intended (a sign a UNION-based injection worked)

What the fix looks like: parameterised queries (prepared statements) treat all user input as data, not SQL syntax. ORM query builders do this by default. Input validation (allow-list) is a defence-in-depth measure but not a substitute for parameterised queries — a determined attacker can bypass many input validation schemes.

// EXAMPLE

sql-injection.test.ts

const injectionPayloads = [
  "' OR '1'='1",
  "'; DROP TABLE users; --",
  "1 UNION SELECT null, null --",
  "admin'--",
];

for (const payload of injectionPayloads) {
  const response = await request.post('/api/login', {
    data: { email: payload, password: 'anything' },
  });
  // Must not be 200 (bypass), must not be 500 (unhandled DB error)
  expect(response.status()).toBe(401);
  expect(await response.text()).not.toContain('SQL');
  expect(await response.text()).not.toContain('syntax error');
}

// WHAT INTERVIEWERS LOOK FOR

Understands parameterised queries as the correct fix (not just input validation). Tests for the correct expected responses — no DB errors, no 500s, no leaked data.

// COMMON PITFALL

Expecting a 400 or 422 for all injection inputs. A well-designed system may return 401 (login failed) rather than 400 (invalid input) — the important thing is that the injection did not succeed and no error details leaked.