Class: CSP::Generator

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

Overview

Provides the ability to generate a content security policy for inline scripts and styles. Will reuse an existing CSP or generate a new one and insert in HEAD.

Instance Method Summary collapse

Constructor Details

#initialize(document_html) ⇒ Generator

Returns a new instance of Generator.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/jekyll-csp/csp.rb', line 15

def initialize(document_html)
  @document_html = document_html
  @nokogiri = Nokogiri::HTML(document_html)

  @csp_tags = {
    "frame-src" => [],
    "script-src" => [],
    "img-src" => [],
    "style-src" => []
  }

  config = Jekyll.configuration({})['jekyll_csp']
  
  @indentation = config['indentation'] || 2
  @enable_newlines = config['newlines'].to_s ? config['newlines'] : true
  @debug = config['debug'].to_s ? config['debug'] : false
  @inject_self = config['inject_self'] || ['script-src', 'style-src', 'img-src', 'frame-src']

  if @enable_newlines == false
    @indentation = 0
  end

  self.write_debug_log(config)
end

Instance Method Details

#convert_all_inline_styles_attributesObject

This function converts elements with style=“color:red” attributes into inline styles



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/jekyll-csp/csp.rb', line 164

def convert_all_inline_styles_attributes
  @nokogiri.css('*').each do |find|
    find_src = find.attr('style')

    if find_src
      if find.attr('id')
        element_id = find.attr('id')
      else
        hash = Digest::MD5.hexdigest find_src + "#{Random.rand(11)}"
        element_id = "csp-gen-" + hash
        find["id"] = element_id
      end

      new_element = "<style>#" + element_id + " { " + find_src + " } </style>"
      find.remove_attribute("style")

      if @nokogiri.at('head')
        @nokogiri.at('head') << new_element
        self.write_debug_log('Converting style attribute to inline style, inserted into HEAD.')
      else
        if @nokogiri.at('body')
          @nokogiri.at('body') << new_element
          Jekyll.logger.info
          self.write_debug_log('Converting style attribute to inline style, inserted into BODY.')
        else
          self.write_debug_log('Unable to convert style attribute to inline style, no HEAD or BODY found.')
        end
      end
    end
  end
end

#find_framesObject

Find all iframes



263
264
265
266
267
268
269
270
271
# File 'lib/jekyll-csp/csp.rb', line 263

def find_frames
  @nokogiri.css('iframe, frame').each do |find|
    find_src = find.attr('src')

    if find_src and find_src.start_with?('http', 'https')
      @csp_tags['frame-src'].push find_src
    end
  end
end

#find_imagesObject

Find all images



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/jekyll-csp/csp.rb', line 198

def find_images
  @nokogiri.css('img').each do |find|
    find_src = find.attr('src')

    if find_src and find_src.start_with?('http', 'https')
      @csp_tags['img-src'].push find_src.match(/(.*\/)+(.*$)/)[1]
    end
  end

  @nokogiri.css('style').each do |find|
    finds = find.content.scan(/url\(([^\)]+)\)/)

    finds.each do |innerFind|
      innerFind = innerFind[0]
      innerFind = innerFind.tr('\'"', '')
      if innerFind.start_with?('http', 'https')
        @csp_tags['img-src'].push self.get_domain(innerFind)
      end
    end
  end

end

#find_inline_stylesObject

Find all inline stylesheets



240
241
242
243
244
# File 'lib/jekyll-csp/csp.rb', line 240

def find_inline_styles
  @nokogiri.css('style').each do |find|
    @csp_tags['style-src'].push self.generate_sha256_content_hash find.content
  end
end

#find_linked_stylesObject

Find all linked stylesheets



248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/jekyll-csp/csp.rb', line 248

def find_linked_styles
  @nokogiri.css('link').each do |find|
    self.write_debug_log(find)
    find_attr = find.attr('href')

    if find_attr
        @csp_tags['style-src'].push find_attr
    else
      self.write_debug_log("Found linked style with no href." << find)
    end
  end
end

#find_scriptsObject

Find all scripts



223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/jekyll-csp/csp.rb', line 223

def find_scripts
  @nokogiri.css('script').each do |find|
    if find.attr('src')
      find_src = find.attr('src')

      if find_src and find_src.start_with?('http', 'https')
        @csp_tags['script-src'].push find_src.match(/(.*\/)+(.*$)/)[1]
      end

    else
      @csp_tags['script-src'].push self.generate_sha256_content_hash find.content
    end
  end
end

#generate_convert_security_policy_meta_tagObject

Creates an HTML content security policy meta tag.



80
81
82
83
84
85
86
87
88
89
# File 'lib/jekyll-csp/csp.rb', line 80

def generate_convert_security_policy_meta_tag
  meta_content = ""

  @csp_tags.each do |tag, items|
    meta_content += self.generate_meta_entry(tag, items)
  end

  csp = self.get_or_create_csp_tag
  csp['content'] = meta_content
end

#generate_meta_entry(tag, items) ⇒ Object

Generate a CSP entry using the correct indentation and formatting



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/jekyll-csp/csp.rb', line 50

def generate_meta_entry(tag, items)
  # Remove duplicates
  items = items.uniq

  # Line separator
  line_sep = @enable_newlines ? "\n" : ""

  if items.empty?
    return "" << line_sep  << self.get_indent_str(3) << tag << ';'
  end

  "" \
  << line_sep  \
  << self.get_indent_str(3) \
  << tag \
  << " " \
  << line_sep \
  << self.get_indent_str(4) \
  << items.join(" " + line_sep + self.get_indent_str(4)) \
  << "; "
end

#generate_sha256_content_hash(content) ⇒ Object

Generate a SHA256 hash from content



280
281
282
283
# File 'lib/jekyll-csp/csp.rb', line 280

def generate_sha256_content_hash(content)
  hash = Digest::SHA2.base64digest content
  "'sha256-#{hash}'"
end

#get_domain(url) ⇒ Object



273
274
275
276
# File 'lib/jekyll-csp/csp.rb', line 273

def get_domain(url)
  uri = URI.parse(url)
  "#{uri.scheme}://#{uri.host}"
end

#get_indent_str(count) ⇒ Object

Get an indentation string.



74
75
76
# File 'lib/jekyll-csp/csp.rb', line 74

def get_indent_str(count)
  " " * (@indentation * count)
end

#get_or_create_csp_tagObject

Locate an existing CSP or create one



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/jekyll-csp/csp.rb', line 92

def get_or_create_csp_tag
  csp = @nokogiri.at_xpath("//meta[translate(@http-equiv, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'content-security-policy']")

  if csp
    return csp
  end

  tag = "<meta http-equiv=\"Content-Security-Policy\" content="">"

  if @nokogiri.at("head")
    self.write_debug_log("Generated content security policy, inserted in HEAD.")
    @nokogiri.at("head") << tag
  elsif @nokogiri.at("body")
    self.write_debug_log("Generated content security policy, inserted in BODY.")
    @nokogiri.at("body") << tag
  else
    self.write_debug_log("Generated content security policy but found no-where to insert it.")
  end

  csp = @nokogiri.at_xpath("//meta[translate(@http-equiv, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'content-security-policy']")
  return csp
end

#inject_defaultsObject

Initialize some default values



154
155
156
157
158
159
160
# File 'lib/jekyll-csp/csp.rb', line 154

def inject_defaults
  @csp_tags.each do |directive, properties|
    if @inject_self.include? directive
      properties.push("'self'")
    end
  end
end

#parse_existing_meta_elementObject

Parse an existing content security policy meta tag



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/jekyll-csp/csp.rb', line 117

def parse_existing_meta_element()
  csp = self.get_or_create_csp_tag

  if csp
    content = csp.attr('content')
    content = content.strip! || content
    policies = content.split(';')
    
    policies.each do |policy|
      policy = policy.strip
      
       policy_tag = policy
       policy_items = []

      if policy.include? ' '
        policy_parts = policy.split(' ')
        policy_tag = policy_parts[0]
        policy_items = policy_parts.drop(1)
      end

      # If an existing tag doesn't exist, add it
      if !@csp_tags.key?(policy_tag)
        @csp_tags[policy_tag] = []
      end
      
      # Concat the tag items
      @csp_tags[policy_tag].concat(policy_items)
    end

    @nokogiri.search('meta[http-equiv="Content-Security-Policy"]').each do |el|
      el.remove
    end
  end
end

#runObject

Builds an HTML meta tag based on the found inline scripts and style hashes



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/jekyll-csp/csp.rb', line 287

def run
  self.parse_existing_meta_element
  self.inject_defaults
  self.convert_all_inline_styles_attributes

  # Find elements in document
  self.find_linked_styles
  self.find_images
  self.find_inline_styles
  self.find_scripts
  self.find_frames

  self.generate_convert_security_policy_meta_tag

  @nokogiri.to_html
end

#write_debug_log(content) ⇒ Object

Write a debug log



42
43
44
45
46
# File 'lib/jekyll-csp/csp.rb', line 42

def write_debug_log(content)
  if @debug
    Jekyll.logger.warn content
  end
end