CSP for Lambda@Edge

之前工作上主要是用 AWS,AWS 放靜態網站有過 CloudFront CDN 時,如果需要調整 header 的話,官方的解決方案是用 Lambda@Edge,寫 AWS Lambda function 的時候,其實我個人有一個偏好,就是能不用第三方 module 就不用,主要原因有兩個,第一個原因是,如果程式碼太大包,會無法在 AWS console 上直接看(或修改)程式碼;第二個原因是發佈流程會比較麻煩,因為還要去安裝 module,然後再全部打包起來上傳。

要調整 header 的一個主要原因就是為了 security headers,大部分的 security header 都還算單純,但是 CSP(Content Security Policy)就複雜很多了,如果沒有用結構化的資料,其實很難維護,但是針對 Lambda function 我又不想要用第三方 module,最後我想到的解決方案,就是設計一個很簡短的工具函式來把結構化的資料轉成 CSP header 的值,這就是我最近趁 COSCUP 2021 會議期間整理好的新的 open source 專案:CSP

這個專案內容就只是一個簡單的 function:

const CSP = (directives) => {
  return directives
    .map((directive) => {
      return `${directive.name} ${directive.value.join(' ')};`;
    })
    .join(' ');
};

不過為了好好設計這個 function 其實我也是花不少功夫,首先就是輸入參數的結構要長怎樣,其實一般比較常見的是用物件 property 直接就作為 directive name 的形式,像是 Google 的 CSP Evaluator

{
  "default-src": ["'none'"],
  "script-src": ["'self'"],
  "connect-src": ["blah", "blah"]
}

這種結構比較精簡,但是問題就是無法保證順序,考慮再三之後,決定還是用陣列的形式:

[
  {
    "name": "default-src",
    "value": ["'none'"]
  },
  {
    "name": "script-src"",
    "value": ["'self'"]
  }
]

這樣就可以讓開發人員確保輸出的順序,其實大部分時候我也不會那麼在意順序,不過要是default-src如果不是第一個感覺就很不舒服。確定主要的資料結構後,再來就是屬性名稱要用什麼好的問題了,為了找到正確的名稱,我去翻了 CSP spec 找到關於 parsing 相關的說明,確定了 spec 定義的結構是這樣的(使用 TypeScript 語法):

type Source = string;

type Directive = {
  name: string;
  value: Source[];
};

type Policy = {
  source: "header" | "meta";
  disposition: "enforce" | "report";
  directiveSet: OrderedSet<Directive>;
};

type Policies = Policy[];

在輸入資料的陣列中,每個元素都是DirectiveDirective的兩個屬性分別是namevaluevalue則是Source的陣列集合,當然Source還有更嚴謹的定義,不過這邊就簡化成字串就好。確定完輸入資料的結構後,就是要想盡辦法簡化 function 的內容了,但是也不希望太難讀懂,調整了幾次變成現在的樣子,我還提供了精簡的版本:

const CSP = p => p.map(d => `${d.name} ${d.value.join(' ')};`).join(' ');

其實我對於那個mapjoin一直耿耿於懷,很想要用reduce解決,但是要避免頭尾多空白,會需要多判斷式,就算不予理會,程式碼長度其實還是比現在這個版本長,結果還是mapjoin看起來比較漂亮,所以最後的版本就維持這樣了。

然後我還寫了測試和提供了兩個 example,分別是 Lambda@Edge 和 Cloudflare Workers 的,不確定還有沒有類似的服務,如果有發現會再加上。最後就是,因為這個 function 設計就是要給人複製貼上的,所以並沒有發布到 npm 上,然後使用 MIT-0 license 所以也不用 attribution,覺得有興趣使用的就請直接複製貼上吧~

PS. 如果有其他需求,可以看看 csp-header,例如 Express 使用,我覺得介面設計得很不錯。