Class: Shugoi::Csp

Inherits:
Object
  • Object
show all
Defined in:
lib/shugoi/csp.rb

Overview

Construction + fusion de la Content-Security-Policy — parité avec csp.ts (module Node). Le module npm pose la CSP fusionnée sur chaque réponse (y compris les pages passées) : la base inclut l'origine de l'API (pour le guard client), les directives additionnelles et, en split-render, unsafe-eval (nécessaire au bootcode eval([...])).

Constant Summary collapse

SHUGOI_ORIGIN =
"https://shugoi.com".freeze

Class Method Summary collapse

Class Method Details

.build(site_key:, api_origin: nil, extra_directives: {}, split_render: true) ⇒ String

Returns header CSP.

Parameters:

  • site_key (String)

    siteKey (parité d'API avec le module Node, inutilisé dans la construction)

  • api_origin (String, nil) (defaults to: nil)

    origine de l'API Shugoi à autoriser (connect-src/script-src…)

  • extra_directives (Hash<String, Array<String>>) (defaults to: {})

    directives additionnelles fusionnées

  • split_render (Boolean) (defaults to: true)

    false → retire 'unsafe-eval' de script-src

Returns:

  • (String)

    header CSP



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/shugoi/csp.rb', line 30

def self.build(site_key:, api_origin: nil, extra_directives: {}, split_render: true)
  api = [SHUGOI_ORIGIN, api_origin].compact.uniq
  merged = {
    "default-src" => ["'self'"],
    "script-src" => ["'self'", "'unsafe-inline'", "'unsafe-eval'", *api],
    "connect-src" => ["'self'", *api],
    "style-src" => ["'self'", "'unsafe-inline'", *api],
    "font-src" => ["'self'", *api, "data:"],
    "img-src" => ["'self'", *api, "data:", "blob:"],
    "frame-ancestors" => ["'self'"],
    "object-src" => ["'none'"],
    "base-uri" => ["'self'"],
    "form-action" => ["'self'"]
  }
  extra_directives.each do |key, values|
    merged[key] = ((merged[key] || []) + Array(values)).uniq
  end
  if split_render == false
    merged["script-src"] = (merged["script-src"] || []).reject { |v| v == "'unsafe-eval'" }
  end
  merged.map { |k, v| "#{k} #{v.join(' ')}" }.join("; ")
end

.merge(existing, added) ⇒ Object

Fusionne une CSP existante (posée par l'app client) avec celle du module. Spec CSP : le mot-clé 'none' doit être SEUL dans une directive — sinon il est ignoré par le navigateur. Lors d'un merge, on garde uniquement 'none' (le plus restrictif).



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/shugoi/csp.rb', line 56

def self.merge(existing, added)
  return added if existing.to_s.empty?
  base = parse(existing)
  parse(added).each do |k, set|
    base[k] = (base[k] || Set.new) | set
  end
  base.each do |k, set|
    base[k] = Set.new(["'none'"]) if set.include?("'none'") && set.size > 1
  end
  base.map { |k, v| "#{k} #{v.to_a.join(' ')}" }.join("; ")
end

.origin_of(base_url) ⇒ Object

Retourne l'origine (schéma://host) d'une base URL, nil sinon.



14
15
16
17
18
19
20
21
22
23
# File 'lib/shugoi/csp.rb', line 14

def self.origin_of(base_url)
  return nil if base_url.to_s.empty?
  u = URI.parse(base_url)
  return nil unless %w[http https].include?(u.scheme)
  origin = "#{u.scheme}://#{u.host}"
  origin += ":#{u.port}" if u.port && u.port != u.default_port
  origin
rescue URI::InvalidURIError
  nil
end

.parse(csp) ⇒ Object

Parse une CSP en Hash<String, Set>.



69
70
71
72
73
74
75
76
77
# File 'lib/shugoi/csp.rb', line 69

def self.parse(csp)
  out = {}
  csp.to_s.split(";").each do |part|
    name, *vals = part.strip.split(/\s+/)
    next if name.nil? || name.empty?
    out[name] = (out[name] || Set.new) | vals
  end
  out
end