developer security

CSP and Alpine.js

Putting my money where my mouth is, I configured a Content Security Policy (CSP) for my blog (amongst others) and in doing so I had to do some minor refactoring.

Shaun Wilde
An alpine mountain

Whilst attempting to convert my blog to use a Content Security Policy (CSP) I was inundated with CSP violations relating to unsafe-eval that was nailed down to the use of the alpine.js package. Now Alpine.js does have a CSP friendly build but it is yet (at the time of writing) to be published to npm. The instructions require you to build and distribute your own copy of alpine.js which isn't ideal but once that hurdle has been overcome it is possible to convert our usage to be CSP friendly. The instructions on alpine.js are rather light in detail about what this entails and so I will detail some of my own findings.

Starting off

The navbar for this site is one of the controls utilising alpine.js and this is what it looked like before conversion.

  <nav
    x-data="{ isOpen: false }"
    class="..."
    @keydown.escape="isOpen = false"
    @click.away="isOpen = false"
  >
    <!--Toggle button (hidden on large screens)-->
    <button
      @click="isOpen = !isOpen"
      type="button"
      class="..."
      :class="{ 'transition transform-180': isOpen }"
      aria-label="Menu"
    >
      <svg
        class="h-6 w-6 fill-current"
        xmlns="http://www.w3.org/2000/svg"
        viewbox="0 0 24 24"
      >
        <path
          x-show="isOpen"
          fill-rule="evenodd"
          clip-rule="evenodd"
          d="..."
        />
        <path
          x-show="!isOpen"
          fill-rule="evenodd"
          d="..."
        />
      </svg>
    </button>
  ...  
  </nav>

Identification of the unsafe-inline violations

The following items cause the unsafe-inline violation to fire and will need moving to external APIs as detailed by alpine.js

  1. { isOpen: false }
  2. !isOpen
  3. isOpen = false
  4. isOpen = !isOpen
  5. { 'transition transform-180': isOpen }

Creating and applying the external API

Following the initial advice and some investigation (okay some judicious logging) the following API was created

Alpine.data("navbar", () => ({
  isOpen: false,

  isClosed() {
    return !this.isOpen;
  },

  closeNavbar() {
    this.isOpen = false;
  },

  toggleNavbar() {
    this.isOpen = !this.isOpen;
  },

  generateNavbarClasses() {
    return { transition: this.isOpen, "transform-180": this.isOpen };
  },

  ...
}));

and wired into the html like so

<nav
    x-data="navbar"
    class="..."
    @keydown.escape="closeNavbar"
    @click.away="closeNavbar"
  >
    <!--Toggle button (hidden on large screens)-->
    <button
      @click="toggleNavbar"
      type="button"
      class="..."
      :class="generateNavbarClasses"
      aria-label="Menu"
    >
      <svg
        class="h-6 w-6 fill-current"
        xmlns="http://www.w3.org/2000/svg"
        viewbox="0 0 24 24"
      >
        <path
          x-show="isOpen"
          fill-rule="evenodd"
          clip-rule="evenodd"
          d="..."
        />
        <path
          x-show="isClosed"
          fill-rule="evenodd"
          d="..."
        />
      </svg>
    </button>
  ...
  </nav>  

Repeating the above steps for other areas now means the site is CSP friendly and unsafe-eval does not need to be applied to the Content Security Policy for the site to work.

As always, your feedback is appreciated

Photo by Stephan Seeber on Unsplash