If a visitor can put text on your page that other visitors will see, and that text is not escaped, they can put a script there instead. It runs as your site, with your visitor's session, and can do anything that visitor can do.

Where it comes from

  • A comment, a name, a review - anything typed by one person and shown to another.
  • A URL parameter printed back onto the page, which is the search box on almost every site.
  • An uploaded file name.
  • Data from an API you do not control.

The habit

Escape on OUTPUT, every time, in the context you are printing into. Not on input - the same data may be printed into HTML, into an attribute and into JavaScript, and each needs different escaping.

echo htmlspecialchars(, ENT_QUOTES, 'UTF-8');
  • In HTML text - htmlspecialchars with ENT_QUOTES.
  • In an attribute - the same, and always quote the attribute.
  • In JavaScript - json_encode, never string concatenation.
  • In a URL - urlencode.
Escaping when the data is SAVED is the classic mistake: the same value then has to be un-escaped for a mail, an export or an API, and one of those paths always forgets. Store what was typed; escape when it is shown.

A second layer

add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;

CSP tells the browser to run scripts only from your own origin, so an injected inline script does not execute even if one gets through. Introduce it in report-only mode first, or you will break your own page.

Test it

Put a harmless marker in every field and look at the SOURCE of the page that shows it. Seeing your marker as text is right; seeing it as a tag is the bug.

A template engine that escapes by default removes most of this class of bug. If yours does, do not reach for the "print raw" helper without a reason you could defend.