live_svelte icon indicating copy to clipboard operation
live_svelte copied to clipboard

Support CSP nonce in script and style tags

Open Darth-Knoppix opened this issue 1 year ago • 4 comments

Context

The svelte Live View component renders <style> and <script> tags inline. This can cause issues when using a content security policy because it's inline.

Opportunity

If additional attributes could be added to these tags, it would allow for a more strict CSP header, specifically adding a nonce-* attribute, reference.

Example output

Header

Content-Security-Policy: style-src 'nonce-imk7oIUnJE8r5ResWI1Rq-TsrtoDqZWTVYQIX9Xf9iU' 'self'; script-src 'nonce-8IBTHwOdqNKAWeKl7plt8g==';

Svelte render output

<script nonce="8IBTHwOdqNKAWeKl7plt8g==">/* some head scripts here */</script>
<div id="MyComponent-17506" data-name="MyComponent" data-props="{}" data-live-json="{}" data-slots="{}" phx-update="ignore" phx-hook="SvelteHook" class="">
  <style nonce="imk7oIUnJE8r5ResWI1Rq-TsrtoDqZWTVYQIX9Xf9iU">/* some styles here*/</style>
  {"var1":null}
  <p>component content</p>
</div>

Darth-Knoppix avatar Jan 09 '24 09:01 Darth-Knoppix

+1 to this. My app is complaining rather verbosely in the chrome console when I load a page with a live svelte component on it

camstuart avatar Jun 07 '24 15:06 camstuart

Couple of question:

  • How would you generate these nonce values in Elixir?
  • These values would be different on every render?
  • Would just implementing arbitrary attributes on style and script tags (might as well do the main div tag too) solve this issue? This would mean you'd have to pass it manually every time for every Svelte component

woutdp avatar Jun 07 '24 15:06 woutdp

Good questions, generating a nonce seems easy enough with:

# endpoint.ex
defmodule MyApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :myapp

  defp generate_nonce() do
    :crypto.strong_rand_bytes(16) |> Base.encode64()
  end

  plug Plug.Static,
    at: "/",
    from: :myapp,
    gzip: false,
    only: MyAppWeb.static_paths(),
    headers: %{
      "content-security-policy" =>
        "default-src 'self'; style-src 'self' 'nonce-" <> generate_nonce() <> "';"
    }
end

but passing that to the svelte component is another matter. I wonder though, can it be assigned to the socket? which is always being passed in 🤔

I don't know why this has cropped up for me all of a sudden. I had my first test app with live_svelte, a fairly sophisticated svelte component doing an address search / autocomplete. I was really impressed, you have created a great tool that fills a gap IMO.

Then I brought that svelte component into a bigger, more serious app which is the one that is complaining. I am going to investigate if there is a package / configuration that has a higher level of paranoia.

In fact, the third party javascript I am calling in inside my svelte, won't even load:

The component:

<script>
  import { onMount } from 'svelte'
  import placekitAutocomplete from '@placekit/autocomplete-js'

  export let live
  export let apiKey
  export let placeHolder
  export let defaultValue

  let pk
  let input

  onMount(() => {
    if (!placeHolder) {
      placeHolder = 'Search...'
    }

    if (input) {
      pk = placekitAutocomplete(apiKey, {
        target: input,
        countries: ['au'],
        placeholder: 'Search for an address',
      })

      pk.configure({
        format: {
          value: item =>
            `${item.name} ${item.city}, ${item.administrative} ${item.zipcode}`,
        },
      })

      pk.on('pick', (value, location, index) => {
        console.log('full address data', location)
        live.pushEvent('geocoded-address', { location }, () => {})
      })
    }
  })
</script>

<input
  type="search"
  bind:this={input}
  class="mt-2 block w-full rounded-md text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 border-zinc-300 focus:border-zinc-400"
  placeholder={placeHolder}
  value={defaultValue}
/>

Gives me these errors in the console:

placekit-autocomplete.esm.mjs:47 Refused to connect to 'https://api.placekit.co/search' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

As well as complaints about inline styles.

I'll keep digging and see what I can come up with

camstuart avatar Jun 07 '24 16:06 camstuart

In case anyone else has similar content security policy (CSP) issues, I was able to work around it like this:

I created a plug in myapp_web/plugs/csp_header.ex

defmodule MyAppWeb.Plugs.CSPHeader do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    csp_header =
      "default-src 'self'; connect-src 'self' https://api.placekit.co; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"

    conn
    |> put_resp_header("content-security-policy", csp_header)
  end
end

And then added it in router.ex to the browser pipeline:

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery

    plug :put_secure_browser_headers, %{
      "content-security-policy" => "default-src 'self'; img-src * data:;"
    }

    plug :fetch_current_user
    plug MyAppWeb.Plugs.CSPHeader
  end

The unsafe aspect is unfortunate, I will need to keep digging on this. But here is a workaround, such as it is.

camstuart avatar Jun 07 '24 16:06 camstuart