Class: Immunio::Template
- Inherits:
-
Object
- Object
- Immunio::Template
- Defined in:
- lib/immunio/plugins/action_view/template.rb
Overview
Renders templates by filtering them through Immunio’s hook handlers.
Constant Summary collapse
- CHECKSUM_CACHE =
Hash.new do |cache, template_id| template = ObjectSpace._id2ref(template_id) if template.respond_to?(:source) && !template.source.nil? finalizer = Immunio::Template.finalize_template(template_id) ObjectSpace.define_finalizer(template, finalizer) cache[template_id] = Digest::SHA1.hexdigest(template.source).freeze end end
- ENCODED_IMMUNIO_TOKENS_RE =
Regexp.compile(/(?:{|%7b)immunio-(var|fragment)(?::|%3a)(\d+)(?::|%3a)([0-9a-f]{1,4})(?:}|%7d)(.*?)(?:{|%7b)(?:\/|%2f)immunio-\1(?::|%3a)\2(?::|%3a)\3(?:}|%7d)/i)
Instance Attribute Summary collapse
-
#vars ⇒ Object
Returns the value of attribute vars.
Class Method Summary collapse
- .current ⇒ Object
- .decode_immunio_tokens(rendered) ⇒ Object
- .finalize_template(id) ⇒ Object
-
.generate_render_var_code(code, escape) ⇒ Object
Generate code injected in templates to wrap everything inside ‘<%= …
- .get_nonce ⇒ Object
-
.mark_and_defer_fragment_write(*args) ⇒ Object
Save fragment info to the root template only.
- .mark_var(content, code, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
- .next_var_id ⇒ Object
- .remove_all_markers!(input) ⇒ Object
- .remove_comment(code) ⇒ Object
- .render_var(code, rendered, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
-
.rendering_stack ⇒ Object
Stack of the templates currently being rendered.
- .vars ⇒ Object
- .wrap_code(code, handler, options = {}) ⇒ Object
- .wrap_code_for_erb(code, options) ⇒ Object
- .wrap_code_for_haml(code, options) ⇒ Object
- .wrap_code_for_slim(code, options) ⇒ Object
Instance Method Summary collapse
- #==(other) ⇒ Object
- #get_nonce ⇒ Object
- #id ⇒ Object
-
#initialize(template) ⇒ Template
constructor
A new instance of Template.
- #is_text? ⇒ Boolean
- #mark_and_defer_fragment_write(key, content, options) ⇒ Object
- #next_template_id ⇒ Object
-
#next_var_id ⇒ Object
Generate the next var unique ID to be used in a template.
- #render(context) ⇒ Object
- #template_sha ⇒ Object
Constructor Details
#initialize(template) ⇒ Template
Returns a new instance of Template.
21 22 23 24 25 26 27 |
# File 'lib/immunio/plugins/action_view/template.rb', line 21 def initialize(template) @template = template @next_var_id = 0 @next_template_id = 0 @vars = {} @scheduled_fragments_writes = [] end |
Instance Attribute Details
#vars ⇒ Object
Returns the value of attribute vars.
19 20 21 |
# File 'lib/immunio/plugins/action_view/template.rb', line 19 def vars @vars end |
Class Method Details
.current ⇒ Object
144 145 146 |
# File 'lib/immunio/plugins/action_view/template.rb', line 144 def current rendering_stack.last end |
.decode_immunio_tokens(rendered) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/immunio/plugins/action_view/template.rb', line 195 def decode_immunio_tokens(rendered) # Look for URI or JS encoded immunio tokens in the rendering and decode them # WebConsole incompatibility: `rendered` can be of type `Mime::Type` Which # doesn't respond to `gsub!`. if rendered.respond_to?(:gsub!) was_html_safe = rendered.html_safe? was_frozen = rendered.frozen? if was_frozen # This is not an airtight solution. Object#dup does not copy methods # defined on the instance, and may be overridden by subclasses to do # things that would cause problems for us. But most likely there is no # problem with using dup. We can't use Object#clone because the clone # retains the frozen status of the original, preventing us from # modifying the string contents. rendered = rendered.dup end rendered.gsub! ENCODED_IMMUNIO_TOKENS_RE, "{immunio-\\1:\\2:\\3}\\4{/immunio-\\1:\\2:\\3}" rendered.instance_variable_set(:@html_safe, true) if was_html_safe rendered.freeze if was_frozen end rendered end |
.finalize_template(id) ⇒ Object
140 141 142 |
# File 'lib/immunio/plugins/action_view/template.rb', line 140 def finalize_template(id) proc { CHECKSUM_CACHE.delete(id) if CHECKSUM_CACHE.has_key?(id) } end |
.generate_render_var_code(code, escape) ⇒ Object
Generate code injected in templates to wrap everything inside ‘<%= … %>`.
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/immunio/plugins/action_view/template.rb', line 305 def generate_render_var_code(code, escape) template = Template.current if template template_id = template.next_template_id handler = template.instance_variable_get(:@template).handler handler_name = if handler.is_a? Class handler.name else handler.class.name end "(__immunio_result = (#{remove_comment(code)}); Immunio::Template.render_var(#{code.strip.inspect}, __immunio_result, #{template_id}, '#{template.template_sha}', __FILE__, __LINE__, #{!!escape}, #{template.is_text?}, '#{handler_name}'))" else code end end |
.get_nonce ⇒ Object
156 157 158 |
# File 'lib/immunio/plugins/action_view/template.rb', line 156 def get_nonce rendering_stack.first.get_nonce end |
.mark_and_defer_fragment_write(*args) ⇒ Object
Save fragment info to the root template only
161 162 163 |
# File 'lib/immunio/plugins/action_view/template.rb', line 161 def mark_and_defer_fragment_write(*args) rendering_stack.first.mark_and_defer_fragment_write(*args) end |
.mark_var(content, code, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/immunio/plugins/action_view/template.rb', line 222 def mark_var(content, code, template_id, template_sha, file, line, escape, is_text, handler) id = Template.next_var_id nonce = Template.get_nonce # NOTE: What happens here is pretty funky to preserve the html_safe SafeBuffer behaviour in ruby. # If escaped is true we directly concatenate the content between two SafeBuffers. This will cause # escaping if content is not itself a SafeBuffer. # Otherwise we explicitly convert to a string, and convert that to a SafeBuffer to ensure that # for instance no escaping is performed on the contents of a <%== %> Erubis interpolation. rendering = if escape && !is_text # explicitly convert (w/ escapes) and mark safe things that aren't String (SafeBuffer is_a String also) # `to_s` is used to render any object passed to a template. # It is called internally when appending to ActionView::OutputBuffer. # We force rendering to get the actual string. # This has no impact if `rendered` is already a string. content = content.to_s.html_safe unless content.is_a? String # As a failsafe, just return the content if it already contains our markers. This can occur when # a helper calls render partial to generate a component of a page. Both render calls are root level # templates from our perspective. if content =~ /\{immunio-var:\d+:#{nonce}\}/ then # don't add markers. Immunio.logger.debug {"WARNING: ActionView not marking interpolation which already contains markers: \"#{content}\""} return content end "{immunio-var:#{id}:#{nonce}}".html_safe + content + "{/immunio-var:#{id}:#{nonce}}".html_safe else content = "" if content.nil? # See comment above if (content.respond_to? :=~) && (content =~ /\{immunio-var:\d+:#{nonce}\}/) # don't add markers. Immunio.logger.debug {"WARNING: ActionView not marking interpolation which already contains markers: \"#{content}\""} return content.html_safe end "{immunio-var:#{id}:#{nonce}}".html_safe + content.to_s.html_safe + "{/immunio-var:#{id}:#{nonce}}".html_safe end # If we got here, the interpolation has been wrapped in our markers and we # need to record send data about it to the hook Template.vars[id.to_s] = { template_sha: template_sha, template_id: template_id.to_s, nonce: nonce, code: wrap_code(code, handler, escape: escape), file: file, line: line } rendering end |
.next_var_id ⇒ Object
148 149 150 |
# File 'lib/immunio/plugins/action_view/template.rb', line 148 def next_var_id rendering_stack.first.next_var_id end |
.remove_all_markers!(input) ⇒ Object
323 324 325 |
# File 'lib/immunio/plugins/action_view/template.rb', line 323 def remove_all_markers!(input) input.gsub!(/\{\/?immunio-(fragment|var):\d+:[a-zA-Z0-9]+\}/, "") end |
.remove_comment(code) ⇒ Object
292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/immunio/plugins/action_view/template.rb', line 292 def remove_comment(code) *, last_line = code.rpartition("\n") comment = Ripper.slice(last_line, "comment") if comment code = code.sub(Regexp.new(Regexp.escape(comment) + "\\Z"), "") end code end |
.render_var(code, rendered, template_id, template_sha, file, line, escape, is_text, handler) ⇒ Object
278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/immunio/plugins/action_view/template.rb', line 278 def render_var(code, rendered, template_id, template_sha, file, line, escape, is_text, handler) rendered = decode_immunio_tokens rendered if rendered.instance_variable_get("@__immunio_processed") then # Ignore buffers marked as __immunio_processed in render as these are full templates or partials return rendered elsif code =~ /yield( .*)?/ # Ignore yielded blocks inside layouts return rendered end rendered = mark_var rendered, code, template_id, template_sha, file, line, escape, is_text, handler rendered.html_safe end |
.rendering_stack ⇒ Object
Stack of the templates currently being rendered.
166 167 168 |
# File 'lib/immunio/plugins/action_view/template.rb', line 166 def rendering_stack Thread.current["immunio.rendering_stack"] ||= [] end |
.vars ⇒ Object
152 153 154 |
# File 'lib/immunio/plugins/action_view/template.rb', line 152 def vars rendering_stack.first.vars end |
.wrap_code(code, handler, options = {}) ⇒ Object
170 171 172 173 174 175 176 177 178 |
# File 'lib/immunio/plugins/action_view/template.rb', line 170 def wrap_code(code, handler, = {}) wrap_method = { 'ActionView::Template::Handlers::ERB' => :wrap_code_for_erb, 'Haml::Plugin' => :wrap_code_for_haml, 'Slim::RailsTemplate' => :wrap_code_for_slim }[handler] send wrap_method, code, end |
.wrap_code_for_erb(code, options) ⇒ Object
180 181 182 183 |
# File 'lib/immunio/plugins/action_view/template.rb', line 180 def wrap_code_for_erb(code, ) modifier = [:escape] ? '=' : '==' "<%#{modifier} #{code} %>" end |
.wrap_code_for_haml(code, options) ⇒ Object
185 186 187 188 |
# File 'lib/immunio/plugins/action_view/template.rb', line 185 def wrap_code_for_haml(code, ) modifier = [:escape] ? '=' : '!=' "#{modifier} #{code}" end |
.wrap_code_for_slim(code, options) ⇒ Object
190 191 192 193 |
# File 'lib/immunio/plugins/action_view/template.rb', line 190 def wrap_code_for_slim(code, ) modifier = [:escape] ? '=' : '==' "#{modifier} #{code}" end |
Instance Method Details
#==(other) ⇒ Object
34 35 36 |
# File 'lib/immunio/plugins/action_view/template.rb', line 34 def ==(other) self.class === other && id == other.id end |
#get_nonce ⇒ Object
59 60 61 62 63 64 |
# File 'lib/immunio/plugins/action_view/template.rb', line 59 def get_nonce # Generate a two byte CSRNG nonce to make our substitutions unpreictable # Why only 2 bytes? The nonce is per render, so the odds of guessing it are very low # and entropy is finite so we don't want to drain the random pool unnecessarily @nonce ||= SecureRandom.hex(2) end |
#id ⇒ Object
29 30 31 32 |
# File 'lib/immunio/plugins/action_view/template.rb', line 29 def id (@template.respond_to?(:virtual_path) && @template.virtual_path) || (@template.respond_to?(:source) && @template.source) end |
#is_text? ⇒ Boolean
38 39 40 |
# File 'lib/immunio/plugins/action_view/template.rb', line 38 def is_text? @template.formats.first == :text end |
#mark_and_defer_fragment_write(key, content, options) ⇒ Object
66 67 68 69 70 71 |
# File 'lib/immunio/plugins/action_view/template.rb', line 66 def mark_and_defer_fragment_write(key, content, ) id = @scheduled_fragments_writes.size nonce = Template.get_nonce @scheduled_fragments_writes << [key, content, ] "{immunio-fragment:#{id}:#{nonce}}#{content}{/immunio-fragment:#{id}:#{nonce}}" end |
#next_template_id ⇒ Object
53 54 55 56 57 |
# File 'lib/immunio/plugins/action_view/template.rb', line 53 def next_template_id id = @next_template_id @next_template_id += 1 id end |
#next_var_id ⇒ Object
Generate the next var unique ID to be used in a template.
47 48 49 50 51 |
# File 'lib/immunio/plugins/action_view/template.rb', line 47 def next_var_id id = @next_var_id @next_var_id += 1 id end |
#render(context) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/immunio/plugins/action_view/template.rb', line 73 def render(context) load_source context # Don't handle templates with no source (inline text templates). unless has_source? rendered = yield rendered.instance_variable_set("@__immunio_processed", true) unless rendered.frozen? return rendered end begin root = true if rendering_stack.length == 0 rendering_stack.push self # Calculate SHA1 of this template. template_sha Immunio.logger.debug {"ActionView rendering template with sha #{@template_sha}, root: #{root}"} rendered = yield rendered.instance_variable_set("@__immunio_processed", true) unless rendered.frozen? if root # This is the root template. Let ActionView render it, and then look # for XSS. # If the rendered result isn't a string, or a string-like, then let's # skip it for safety sake. unless rendered.respond_to? :to_str unless $__immunio_av_rendered_non_string Immunio.logger.warn { "ActionView rendered #{@template.inspect} to a non-string-like value: #{rendered.inspect}. This rendering will not be analyzed for XSS. Further warnings will be suppressed." } $__immunio_av_rendered_non_string = true end return rendered end rendered = rendered.to_str result = run_hook!("template_render_done", { content_type: Mime::Type.lookup_by_extension(@template.formats.first).to_s, rendered: rendered, vars: @vars }) # We use the return value from the hook handler if present. rendered = result["rendered"] || rendered.dup remove_var_markers! rendered # If some fragments were marked to be cached, commit their content to cache. write_and_remove_fragments! context, rendered rendered.html_safe else # This is a partial template. Just render it. rendered end ensure top_template = rendering_stack.pop unless top_template == self raise Error, "Unexpected Immunio::Template on rendering stack. Expected #{id}, got #{top_template.try :id}." end end end |
#template_sha ⇒ Object
42 43 44 |
# File 'lib/immunio/plugins/action_view/template.rb', line 42 def template_sha CHECKSUM_CACHE[@template.object_id] end |