Currently accepting new security engagementsRequest a scope
Redacted engagementCriticalSQL injection and stored cross-site scripting

SQL injection and stored cross-site scripting behind an authenticated interface

Two classic injection classes, both reachable only after login, which is why neither had ever been reported by an unauthenticated scan. The database defect exposed data across the whole application. The scripting defect ran in the browser of whoever reviewed the affected record, which in this product was always an administrator.

A web application with a reporting interface and user generated content, tested with valid low-privilege credentials

Redacted. This describes work that was carried out. The client, the product, the affected endpoints and the exact requests are deliberately absent, and no detail here would help anyone locate or reproduce the issue on the original system. Published with the class and the reasoning intact because that is the part you can act on.

The situation

Most automated testing on this product had been unauthenticated, so it had only ever exercised the marketing pages and the login form. Testing here started from a normal low-privilege account, which is the position a real attacker reaches by signing up.

How it was approached

  1. Enumerated the parameters an authenticated user can influence, including sort and filter fields, export options and pagination, which are frequently concatenated into queries because they are assumed to be internal.
  2. Confirmed the database defect through behaviour rather than error messages, so the finding did not depend on verbose errors that a later configuration change might hide.
  3. Traced every place user supplied content is stored and then rendered again, and tested each render context separately, since HTML, attribute and script contexts each require different encoding.
  4. Verified who would actually see the injected content in normal operation, because that is what decides severity.

What was found

One reporting parameter reached the database without parameterisation, allowing controlled query behaviour and read access beyond the tenant the account belonged to. Separately, a free text field was stored unencoded and rendered into an administrative review screen, so content submitted by an ordinary user executed in an administrator's session.

Why it mattered

The first gave read access to data belonging to other customers from an account that only ever paid for its own. The second gave an ordinary user a path to actions in an administrator's browser, in their session, with their permissions. Chained, a self-registered account reaches the top of the application.

How it was fixed

  • Use parameterised queries everywhere without exception, and treat any query built by string concatenation as a defect regardless of whether the input looks internal.
  • Validate sort and filter parameters against an allow list of column names rather than escaping whatever arrives.
  • Encode on output, correctly for the context being written into, and treat any stored value as untrusted no matter which role stored it.
  • Add a Content Security Policy so that a missed encoding does not immediately become script execution.
  • Run the database connection with least privilege so a query defect cannot read tables the feature never needed.
Check this on your own system

Three things worth doing this week

None of these needs a purchase order. They are the checks that most often turn up something on a system nobody has looked at from the outside.

Unauthenticated scanning tests the front door of an empty building

If your product is behind a login, the interesting surface is behind that login too. Testing needs credentials for every role, or it is not testing your product.

Severity depends on who sees the payload

Stored cross-site scripting that only ever renders back to its author is a low finding. The same defect rendering into an administrator's screen is a path to the whole system. The difference is workflow, not code.

Internal parameters are still user input

Sort fields, column names, export selectors and pagination values arrive from the browser. They are as controllable as a search box and are far less likely to have been reviewed.

More in this area

Penetration testing

All case studies
Redacted engagement

OAuth misconfiguration that allowed one account to be linked to another

Single sign-on was configured so that the identity returned by the provider was trusted more than the account it was being attached to. The result was an account linking path that let one identity end up in control of a different user's account. This is the class of issue that reads as a configuration detail and behaves as a full account takeover.

Redacted engagement

Password reset and session handling gaps found by testing the whole account lifecycle

Reviewing authentication as a lifecycle rather than a login page surfaced several issues that individually looked minor and together produced a reliable way to hold access to an account after the owner had tried to lock it down. Most of these do not appear in scanner output at all, because every response involved is a legitimate one.

Want to know whether this applies to you?

Tell us what you are running and which of these worries you. We will tell you plainly whether it is worth testing, what it would cost, and when the answer is that you should fix something yourself first.