woutdp / live_svelte

Svelte inside Phoenix LiveView with seamless end-to-end reactivity
https://hexdocs.pm/live_svelte
MIT License
1.01k stars 37 forks source link

Support CSP nonce in script and style tags #102

Open Darth-Knoppix opened 5 months ago

Darth-Knoppix commented 5 months ago

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>
camstuart commented 3 weeks ago

+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

woutdp commented 3 weeks ago

Couple of question:

camstuart commented 3 weeks ago

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 commented 3 weeks ago

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.