skip to content

In a hotel loyalty statement page's Content-Security-Policy, what does `'unsafe-inline'` in `script-src` actually permit to run?

level: juniorimportance: must knowfreq 74%

answer

  1. a behaviour, not a source
  2. three inline positions, not one
  3. handler attributes count as script
  4. javascript: targets are inline too
  5. external loads are still matched

basics

~20 s

'unsafe-inline' switches off the inline check for that directive's type. In a script source list it permits three positions at once: inline script element text, event-handler content attributes such as onclick, and javascript: navigation targets.

solid answer

~50 s

`'unsafe-inline'` is a `keyword-source`, but it grants a behaviour rather than a source: no URL is ever matched against it. It is read by the algorithm a conforming browser runs to answer *does a source list allow all inline behavior for type*, and its whole job is to answer yes. For a script source list that covers three distinct positions - the text of an inline `<script>` element, event-handler content attributes such as `onclick` and `onerror`, and `javascript:` navigation targets. For a style source list it covers `<style>` element text and the `style` content attribute. So the statement page still refuses a `<script src>` from an unlisted host, but any injected text landing in one of those inline positions runs in the page's own origin. One caveat: if the same source list carries a `nonce-source` or a `hash-source`, a CSP Level 2 or later browser ignores `'unsafe-inline'` entirely.

code

http · 3 lines
http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'

go deeper

for a junior

Remember that the keyword covers three inline positions, not just script element text: element text, event-handler content attributes, and javascript: targets. Saying only the first is the common half-answer.

for a middle

Explain that it is a keyword-source no URL is matched against, that the check is parameterised by inline type, and that external fetches are still restricted by the rest of the list.

for a senior

In an audit, read the whole source list before judging the keyword: a nonce or hash in the same list, or 'strict-dynamic' in a script list, can make it dead text while it still reads as a hole.

for a principal

The question a lead owns is what the policy is buying at all once inline is open: if injected text executes, the remaining value is limited to controlling where external script comes from, and that has to be weighed against the cost of closing it.

## The keyword that matches nothing A `Content-Security-Policy` is a semicolon-separated list of directives, and each fetch directive carries a whitespace-separated `serialized-source-list`. Most entries in that list are things a URL is compared against: a `host-source` such as `https://static.example.net`, a `scheme-source` such as `data:`, or the `keyword-source` `'self'`. `'unsafe-inline'` is a keyword-source too, but **no URL is ever matched against it**. It plays no part in deciding whether a `<script src>` may be fetched. It is read by a different algorithm - the one a conforming browser runs to answer *"Does a source list allow all inline behavior for type?"* - and its entire job is to make that answer yes. The words **for type** are what candidates skip, and they are the answer to this question. ## The three positions "inline" covers for script Inline behaviour is not one thing. Three separate positions are governed by it, and the keyword opens all three at once: - **The text of an inline `<script>` element** - the case everyone means when they say inline script. - **Event-handler content attributes** - `onclick`, `onerror`, `onload` and the rest. The attribute *value* is compiled and run, so an injected `<img src=x onerror=...>` is script execution with no `<script>` element anywhere on the page. - **`javascript:` navigation targets** - an `href` or a form target whose URL scheme is `javascript:`. Following it runs the remainder of the URL as code. A style source list has its own pair: the text of a `<style>` element, and the `style` content attribute on an element. ## What the policy still enforces The keyword is not a general amnesty: - **External script is still restricted.** A `<script src="https://unlisted.example/x.js">` is still matched against the source list, and still refused when nothing matches. - **Turning a string into code is not granted by it.** That is a separate keyword, `'unsafe-eval'`, and the two are independent of each other. - **Other directives are untouched.** `connect-src`, `base-uri` and the rest keep whatever they were doing. The honest summary of `script-src 'self' 'unsafe-inline'` on the statement viewer is therefore: the policy has kept control of **where script is loaded from** and given up control of **whether injected text becomes code**. Since a Content-Security-Policy is a second line of defence written on the assumption that an injection already happened and the output encoding already failed, that is most of what was being bought. | Expression in `script-src` | What it grants | |---|---| | `'self'` | script fetched from the document's own scheme, host and port | | `https://static.example.net` | every response that host can be made to return | | `'unsafe-inline'` | all inline behaviour for the script type: element text, handler attributes, `javascript:` targets | | `'unsafe-eval'` | turning strings into code, for the whole document | | `data:` | a script whose body travels inside the URL itself | ## The narrower keyword standing next to it CSP Level 3 adds `'unsafe-hashes'`, which is often misread as a milder `'unsafe-inline'`. It is narrower and far more specific: it lets a `hash-source` already in the list match positions a hash normally cannot reach - event-handler content attributes, `style` content attributes, and `javascript:` navigation targets. Nothing whose hash is absent from the list is permitted by it. Its residual risk is its own, and it is the part worth carrying into an audit: **a hash permits a particular script text to run, not to run where the author meant it to run.** If one of the hashed handlers performs a privileged action - exporting the statement, confirming a points transfer - then an attacker who can inject markup can place that exact text on an element of their own choosing, and it will match the hash and run. ## Why a policy carrying it may be doing nothing at all Two rules can leave `'unsafe-inline'` as dead text in a list that still contains it, which is why an audit reads the whole source list before judging the keyword: 1. If the same source list also carries any `nonce-source` or `hash-source`, a browser implementing CSP Level 2 or later ignores `'unsafe-inline'`. 2. If a script source list carries `'strict-dynamic'`, a CSP Level 3 browser ignores it for the script types. So the keyword's presence tells you nothing on its own, and neither does its absence tell you the inline positions are closed - a wildcard or a scheme-wide source elsewhere in the same list can still be the way in. Read the list, then decide whether this policy stops an injected string from executing, or whether it never did.

  • What does `'unsafe-hashes'` unlock that a plain `hash-source` cannot, and what risk remains?
    It lets a hash already in the list match event-handler content attributes, `style` content attributes and `javascript:` navigation targets - positions a hash otherwise never matches. The residual risk is placement: a hash permits that exact script text, not the element it was written on, so an attacker who can inject markup can attach a hashed privileged handler to something else and it still matches.
  • The statement page locks down script but leaves `'unsafe-inline'` in `style-src`. Does that matter?
    It permits `<style>` element text and `style` content attributes, so injected markup can restyle the page: overlay or hide controls for a redress attack, or make selector-driven requests that probe what is on the page. It does not execute script, so it is a smaller hole than the script-side keyword - but it is not nothing, and it is usually left in by accident rather than by decision.
  • Does `'unsafe-inline'` let an injected script tag load from any host?
    No. Fetching an external script is matched against the source list as usual, and the keyword matches no URL, so `<script src>` pointing at a host nothing in the list covers is still refused. The keyword only concerns code that is already in the document as text.

saying these in an interview costs you the question

  • Thinks the keyword affects only inline script element text
  • Says an injected onerror handler is still blocked by the policy
  • Believes it also permits compiling strings into code
  • Calls it a source expression matching the page's own origin
  • Treats it as harmless because 'self' is also listed