Giter Club home page Giter Club logo

better-html's Introduction

Improve html in your Rails app.

This gem replaces the normal ERB parsing with an HTML-aware ERB parsing. This makes your templates smarter by adding runtime checks around the data interpolated from Ruby into HTML.

How to use

Add better-html to your Gemfile with its dependency:

gem "better_html"

Helpers

If you want to use html_attributes helper as described further down, add it to your app/helpers/application_helper.rb,

module ApplicationHelper
  include BetterHtml::Helpers

  ...

Configuration

A global configuration for the app is stored at BetterHtml.config. The default configuration can be changed like this:

# config/initializers/better_html.rb
BetterHtml.configure do |config|
  config.allow_single_quoted_attributes = false
end

or if you prefer storing the config elsewhere, in a yml file for example:

# config/initializers/better_html.rb
BetterHtml.config = BetterHtml::Config.new(YAML.load(File.read('/path/to/.better-html.yml')))

Available configuration options are:

  • partial_tag_name_pattern: Regex to validate foo in <foo>. Defaults to /\A[a-z0-9\-\:]+\z/.
  • partial_attribute_name_pattern: Regex to validate bar in <foo bar=1>. Defaults to /\A[a-zA-Z0-9\-\:]+\z/.
  • allow_single_quoted_attributes: When true, <foo bar='1'> is valid syntax. Defaults to true.
  • allow_unquoted_attributes: When true, <foo bar=1> is valid syntax. Defaults to false.
  • javascript_safe_methods: List of methods that return javascript-safe strings. This list is used by SafeErbTester when determining whether ruby interpolation is safe for a given attribute. Defaults to ['to_json'].
  • lodash_safe_javascript_expression: Same as javascript_safe_methods, but for lodash templates. Defaults to [/\AJSON\.stringify\(/].
  • javascript_attribute_names: List of all attribute names that contain javascript code. This list is used by SafeErbTester when determining whether or not a given attribute value will be eval'ed as javascript. Defaults to [/\Aon/i] (matches onclick for example).
  • template_exclusion_filter: This is called when determining whether to apply runtime checks on a .erb template. When this Proc returns false, no safety checks are applied and parsing is done using the default Rails erubi engine. For example, to exclude erb templates provided by libraries, use: Proc.new { |filename| !filename.start_with?(Rails.root.to_s) }. Defaults to nil (all html.erb templates are parsed).

By default, only files named .html.erb are parsed at runtime using BetterHtml's erubi implementation. To change this behavior and parse other file types, assign the erubi implementation into BetterHtml::BetterErb.content_types like this:

# config/initializers/better_html.rb
impl = BetterHtml::BetterErb.content_types['html.erb']
BetterHtml::BetterErb.content_types['htm.erb'] = impl
BetterHtml::BetterErb.content_types['atom.erb'] = impl
BetterHtml::BetterErb.content_types['html+variant.erb'] = impl

Syntax restriction

In order to apply effective runtime checks, it is necessary to enforce the validity of all HTML contained in an application's templates. This comes with an opinionated approach to what ERB syntax is allowed given any HTML context. The next section describes the allowed syntax.

Use ruby expressions inside quoted html attributes.

Allowed ✅
<img class="<%= value %>">

Not allowed ❌
<img <%= value %>>

Not allowed ❌
<img class=<%= value %>>

Use interpolation into tag or attribute names.

Allowed ✅
<img data-<%= value %>="true">

Allowed ✅
<ns:<%= value %>>

Not allowed ❌ (missing space after closing quote)
<img class="hidden"<%= value %>>

Not allowed ❌
<img <%= value %>="true">

Insert conditional attributes using html_attributes helper.

Allowed ✅
<img <%= html_attributes(class: 'hidden') if condition? %>>

Not allowed ❌
<img <% if condition? %>class="hidden"<% end %>>

Only insert expressions (<%= or <%==) inside script tags, never statements (<%)

<script>
  // Allowed ✅
  var myValue = <%== value.to_json %>;
  if(myValue)
    doSomething();

  // Not allowed ❌
  <% if value %>
    doSomething();
  <% end %>
</script>

Runtime validations of html attributes

Looking only at a ERB file, it's impossible to determine if a given Ruby value is safe to interpolate. For example, consider:

<img class="<%= value %>">

Assuming value may not be escaped properly and could contain a double-quote character (") at runtime, then the resulting HTML would be invalid, and the application would be vulnerable to XSS when value is user-controlled.

With HTML-aware ERB parsing, we wrap value into a runtime safety check that raises and exception when value contains a double-quote character that would terminate the html attribute. The safety check is performed after normal ERB escaping rules are applied, so the standard html_safe helper can be used.

The html_attributes helper works the same way, it will raise when attribute values are escaped improperly.

Runtime validations of tag and attribute names

Consider the following ERB template

<img data-<%= value %>="true">

When value is user-controlled, an attacker may achieve XSS quite easily in this situation. We wrap value in a runtime check that ensures it only contains characters that are valid in an attribute name. This excludes =, / or space, which should prevent any risk of injection.

The html_attributes helper works the same way, it will raise when attribute names contain dangerous characters.

Runtime validations of "raw text" tags (script, textarea, etc)

Consider the following ERB template:

<textarea>
  <%== value %>
</textarea>

In circumstances where value may contain input such as </textarea><script>alert(1)</script>, an attacker can easily achieve XSS. We make best-effort runtime validations on this value in order to make it safe against some obvious attacks.

We check for any interpolation containing </textarea and raise an exception if this substring occurs. Note that this won't catch cases where an end tag is split across multiple adjacent interpolations.

The same strategy is applied to other tags which contain non-html data, such as <script>, html comments and CDATA tags.

Testing for valid HTML and ERB

In addition to runtime validation, this gem provides test helpers that makes it easy to write a test to assert .to_json is used in every script tag and every html attribute which end up being executed as javascript (onclick and similar). The main goal of this helper is to assert that Ruby data translates into Javascript data, but never becomes javascript code.

Simply create test/unit/erb_safety_test.rb and add code like this:

# frozen_string_literal: true

require 'test_helper'
require 'better_html/test_helper/safe_erb_tester'

class ErbSafetyTest < ActiveSupport::TestCase
  include BetterHtml::TestHelper::SafeErbTester
  ERB_GLOB = Rails.root.join(
    'app', 'views', '**', '{*.htm,*.html,*.htm.erb,*.html.erb,*.html+*.erb}'
  )

  Dir[ERB_GLOB].each do |filename|
    pathname = Pathname.new(filename).relative_path_from(Rails.root)
    test "missing javascript escapes in #{pathname}" do
      assert_erb_safety(File.read(filename), filename:)
    end
  end
end

You may also want to assert that all .html.erb templates are parseable, to avoid deploying broken templates to production. Add this code in test/unit/erb_implementation_test.rb

# frozen_string_literal: true

require 'test_helper'

class ErbImplementationTest < ActiveSupport::TestCase
  ERB_GLOB = Rails.root.join(
    'app', 'views', '**', '{*.htm,*.html,*.htm.erb,*.html.erb,*.html+*.erb}'
  )

  Dir[ERB_GLOB].each do |filename|
    pathname = Pathname.new(filename).relative_path_from(Rails.root)
    test "html errors in #{pathname}" do
      data = File.read(filename)
      BetterHtml::BetterErb::ErubiImplementation.new(data, filename:).validate!
    end
  end
end

If you're using RSpec you can add the following code to spec/better_html_spec.rb

# frozen_string_literal: true

require "rails_helper"

RSpec.describe "BetterHtml" do
  it "does assert that all .html.erb templates are parseable" do
    erb_glob = Rails.root.join(
      "app", "views", "**", "{*.htm,*.html,*.htm.erb,*.html.erb,*.html+*.erb}"
    )

    Dir[erb_glob].each do |filename|
      data = File.read(filename)
      expect {
        BetterHtml::BetterErb::ErubiImplementation.new(data, filename:).validate!
      }.not_to raise_exception
    end
  end
end

Working with the ERB parser

This gem provides an ERB parser that builds an AST from HTML+ERB templates. Unlike higher-level libraries like Nokogiri, this parser does not make assumptions about the validity of HTML documents (for example, opening tags being matched with closing tags). The parser also handles ERB tags as first class nodes in the syntax tree.

require 'better_html/parser'

buffer = Parser::Source::Buffer.new('(buffer)')
buffer.source = '<div><%= value -%></div>'
parser = BetterHtml::Parser.new(buffer)

puts parser.inspect
# => #<BetterHtml::Parser ast=s(:document,
#   s(:tag, nil,
#     s(:tag_name, "div"), nil, nil),
#   s(:text,
#     s(:erb,
#       s(:indicator, "="), nil,
#       s(:code, " value "),
#       s(:trim))),
#   s(:tag,
#     s(:solidus),
#     s(:tag_name, "div"), nil, nil))>

The syntax tree exposed by this parser is not to be confused with the nested nature of HTML elements. At this stage, the parser does not build html elements, only tags which mark the beginning and end of elements.

better-html's People

Contributors

alec-c4 avatar bslobodin avatar clayton-shopify avatar dependabot[bot] avatar dougedey-shopify avatar edouard-chin avatar einstein- avatar etiennebarrie avatar eugeneius avatar flavorjones avatar geoffharcourt avatar iainbeeston avatar jhawthorn avatar joelhawksley avatar jonpulsifer avatar meagar avatar muan avatar nickcampbell18 avatar petergoldstein avatar peterzhu2118 avatar pgrimaud avatar rafaelfranca avatar remvee avatar shopify-rails[bot] avatar thegedge avatar tomstuart avatar yaworsk avatar yykamei avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

better-html's Issues

Convert AST back into Erb

Is there a way to convert from the AST back into ERB?

Given something like this:

    require 'better_html/parser'
    code = File.read(Rails.root.join('tmp', 'apps', 'davy-jones', 'app', 'views', 'posts', 'index.html.erb'))
    buffer = Parser::Source::Buffer.new('(buffer)')
    buffer.source = code
    parser = BetterHtml::Parser.new(buffer)
    ast = parser.ast

Is there a way to do `ast.to_erb'?

undefined local variable or method `reset_mailer' RSpec::ExampleGroups::BetterHtml

Hi!
I have following test case:

# frozen_string_literal: true

require "rails_helper"

RSpec.describe "BetterHtml" do
  it "does assert that all .html.erb templates are parseable" do
    erb_glob = Rails.root.join(
      "app", "views", "**", "{*.htm,*.html,*.htm.erb,*.html.erb,*.html+*.erb}"
    )

    Dir[erb_glob].each do |filename|
      data = File.read(filename)
      expect { BetterHtml::BetterErb::ErubiImplementation.new(data).validate! }.not_to raise_exception
    end
  end
end

but after upgrade email_spec from 2.2.0 to 2.2.1 i've got following error:


NameError: undefined local variable or method `reset_mailer' for #<RSpec::ExampleGroups::BetterHtml "does assert that all .html.erb templates are parseable" (./spec/better_html_spec.rb:6)>

  0) BetterHtml does assert that all .html.erb templates are parseable
     Failure/Error: super

     NameError:
       undefined local variable or method `reset_mailer' for #<RSpec::ExampleGroups::BetterHtml "does assert that all .html.erb templates are parseable" (./spec/better_html_spec.rb:6)>
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-expectations-3.11.1/lib/rspec/matchers.rb:965:in `method_missing'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:767:in `method_missing'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/email_spec-2.2.1/lib/email_spec/rspec.rb:6:in `block (2 levels) in <top (required)>'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:457:in `instance_exec'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:457:in `instance_exec'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:365:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:529:in `block in run_owned_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:528:in `each'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:528:in `run_owned_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:615:in `block in run_example_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:614:in `reverse_each'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:614:in `run_example_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:484:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:505:in `run_before_example'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:261:in `block in run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:468:in `block in with_around_example_hooks'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:486:in `block in run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:626:in `block in run_around_example_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:352:in `call'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-rails-5.1.2/lib/rspec/rails/adapters.rb:75:in `block (2 levels) in <module:MinitestLifecycleAdapter>'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:457:in `instance_exec'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:457:in `instance_exec'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:390:in `execute_with'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:628:in `block (2 levels) in run_around_example_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:352:in `call'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:629:in `run_around_example_hooks_for'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:486:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:468:in `with_around_example_hooks'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:511:in `with_around_and_singleton_context_hooks'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:259:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:646:in `block in run_examples'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:642:in `map'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:642:in `run_examples'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:607:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:121:in `map'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/configuration.rb:2068:in `with_suite_hooks'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:116:in `block in run_specs'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/reporter.rb:74:in `report'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:115:in `run_specs'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:89:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:71:in `run'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:45:in `invoke'
     # /Users/alec/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rspec-core-3.11.0/exe/rspec:4:in `<main>'
     # 
     #   Showing full backtrace because every line was filtered out.
     #   See docs for RSpec::Configuration#backtrace_exclusion_patterns and
     #   RSpec::Configuration#backtrace_inclusion_patterns for more information.

What's wrong? Other tests works fine

Ruby 2.7 Warnings

If a project is on Ruby 2.7 there is a deprecation warning about keyword parameters. Example:

/Users/michaelmenanno/.gem/ruby/2.7.1/gems/better_html-1.0.14/lib/better_html/test_helper/safe_erb_tester.rb:42: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/michaelmenanno/.gem/ruby/2.7.1/gems/better_html-1.0.14/lib/better_html/parser.rb:23: warning: The called method `initialize' is defined here

Is there a way to disable specific rules for only some lines?

When I found this gem the other day, I was very impressed with its rigor.
However, when I develop, I really want to disable certain rules on one line in relation to other gems (I know it's best not to disable them).
Is there any way?
Here is a concrete example of displaying by RSpec, and The gem that causes it is ranked-model.

     Failure/Error: <%= render partial: 'procedure', collection: @registered_procedures, as: 'procedure' %>

     ActionView::Template::Error:
       Invalid attribute name "data-model_name" does not match regular expression /\A[a-zA-Z0-9\-\:]+\z/
       On line 2 column 22:
       <tr class="procedure" data-model_name="
                             ^^^^^^^^^^^^^^^
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/runtime_checks.rb:135:in `check_attribute_name'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/runtime_checks.rb:112:in `check_token'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/erubi_implementation.rb:24:in `block in add_text'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/erubi_implementation.rb:23:in `parse'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/erubi_implementation.rb:23:in `add_text'
     # /usr/local/bundle/gems/erubi-1.12.0/lib/erubi.rb:170:in `block in initialize'
     # /usr/local/bundle/gems/erubi-1.12.0/lib/erubi.rb:137:in `scan'
     # /usr/local/bundle/gems/erubi-1.12.0/lib/erubi.rb:137:in `initialize'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/runtime_checks.rb:11:in `initialize'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb.rb:55:in `new'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb.rb:55:in `generate'
     # /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb.rb:25:in `call'
     # ./app/views/managers/procedures/edit.html.erb:58:in `_app_views_managers_procedures_edit_html_erb__3675796525123150876_107280'
     # /usr/local/bundle/gems/benchmark-0.2.1/lib/benchmark.rb:311:in `realtime'
     # ./app/controllers/managers/procedures_controller.rb:33:in `update'
     # /usr/local/bundle/gems/actiontext-6.1.7.2/lib/action_text/rendering.rb:20:in `with_renderer'
     # /usr/local/bundle/gems/actiontext-6.1.7.2/lib/action_text/engine.rb:59:in `block (4 levels) in <class:Engine>'
     # /usr/local/bundle/gems/warden-1.2.9/lib/warden/manager.rb:36:in `block in call'
     # /usr/local/bundle/gems/warden-1.2.9/lib/warden/manager.rb:34:in `catch'
     # /usr/local/bundle/gems/warden-1.2.9/lib/warden/manager.rb:34:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/tempfile_reaper.rb:15:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/etag.rb:27:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/conditional_get.rb:40:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/head.rb:12:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/session/abstract/id.rb:266:in `context'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/session/abstract/id.rb:260:in `call'
     # /usr/local/bundle/gems/railties-6.1.7.2/lib/rails/rack/logger.rb:37:in `call_app'
     # /usr/local/bundle/gems/railties-6.1.7.2/lib/rails/rack/logger.rb:26:in `block in call'
     # /usr/local/bundle/gems/railties-6.1.7.2/lib/rails/rack/logger.rb:26:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/method_override.rb:24:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/runtime.rb:22:in `call'
     # /usr/local/bundle/gems/rack-2.2.6.2/lib/rack/sendfile.rb:110:in `call'
     # /usr/local/bundle/gems/railties-6.1.7.2/lib/rails/engine.rb:539:in `call'
     # /usr/local/bundle/gems/rack-test-2.0.2/lib/rack/test.rb:358:in `process_request'
     # /usr/local/bundle/gems/rack-test-2.0.2/lib/rack/test.rb:155:in `request'
     # ./spec/requests/managers/procedures_spec.rb:178:in `block (4 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # BetterHtml::HtmlError:
     #   Invalid attribute name "data-model_name" does not match regular expression /\A[a-zA-Z0-9\-\:]+\z/
     #   On line 2 column 22:
     #   <tr class="procedure" data-model_name="
     #                         ^^^^^^^^^^^^^^^
     #   /usr/local/bundle/gems/better_html-2.0.1/lib/better_html/better_erb/runtime_checks.rb:135:in `check_attribute_name'

Better HTML breaks Rails error page when web-console gem is used

Hey folks 👋

When @clayton-shopify hooked us up with better_html in https://github.com/Shopify/help/pull/7582, it introduced a bug that was fixed in https://github.com/Shopify/help/pull/7839.

What ended up happening was that when the config.allow_single_quoted_attributes = false is set and the web-console gem is installed, anytime an error occurs when developing locally causes the Rails error page to fail to render. https://github.com/Shopify/help/pull/7839 has a bit more of a description of this problem.

The one solution I discovered from the better-html README was to exclude running better-html on any of the ERB templates from the app's gems via:

BetterHtml.configure do |config|
  config.allow_single_quoted_attributes = false
  config.template_exclusion_filter = Proc.new { |filename| !filename.start_with?(Rails.root.to_s) }
end

I wasn't able to figure out a way to exclude only the web-console gem's ERB templates. The following method didn't work:

BetterHtml.configure do |config|
  config.allow_single_quoted_attributes = false
  config.template_exclusion_filter = proc do |filename|
    filename.include?('web_console')
  end
end

Let me know if there's any extra info I can provide 😄

Documentation: explain XSS vector

In the README for Runtime validations of "raw text" tags it says this, but it's hard to tell what is meant by "easily":

var myValue = <%== value.to_json %>;

...where value may contain input such as </script><script>, an attacker can easily achieve XSS...

The to_json method replaces all HTML entity characters with their \u encoding. Enabling active_support.escape_html_entities_in_json is the default setting since Rails 4.2.

Is there still some way that this output could contain HTML characters? Or should we amend this to not say "easily" anymore and explain the actual risks here.

I suppose it would be possible to use some object with its own implementation of to_json that is not as safe as ActiveSupport to actually hit this. Was that the intended explanation, and is better-html able to test this somehow at runtime? (I only see these tests, which also claim that unsafe.to_json is actually safe.)

ruby_memcheck reports lost bytes

Hello! I'm running ruby_memcheck on some gems with native extensions, and this is the output I got here for the default test suite:

112 bytes in 1 blocks are definitely lost in loss record 30,901 of 46,629
  malloc (at /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
  objspace_xmalloc0 (gc.c:11465)
  rb_ary_transient_heap_evacuate_ (array.c:407)
  rb_ary_transient_heap_evacuate_ (array.c:390)
  rb_ary_transient_heap_evacuate (array.c:425)
  transient_heap_block_evacuate.isra.0 (transient_heap.c:730)
  transient_heap_evacuate (transient_heap.c:807)
  transient_heap_evacuate (transient_heap.c:776)
  rb_postponed_job_flush (vm_trace.c:1728)
  rb_threadptr_execute_interrupts (thread.c:2444)
  rb_threadptr_execute_interrupts (thread.c:2417)
  rb_vm_check_ints (vm_core.h:2003)
  rb_vm_check_ints (vm_core.h:1999)
  vm_pop_frame (vm_insnhelper.c:415)
  rb_vm_pop_frame (vm_insnhelper.c:424)
  vm_call_cfunc_with_frame (vm_insnhelper.c:3041)
  vm_sendish (vm_insnhelper.c:4751)
  vm_exec_core (insns.def:778)
  rb_vm_exec (vm.c:2211)
 *tokenizer_yield_tag (tokenizer.c:138)
 *tokenizer_callback (tokenizer.c:145)
 *scan_rawtext (tokenizer.c:605)
 *scan_once (tokenizer.c:643)
 *tokenizer_scan_all (tokenizer.c:660)
 *tokenizer_tokenize_method (tokenizer.c:704)
  vm_call_cfunc_with_frame (vm_insnhelper.c:3037)
  vm_sendish (vm_insnhelper.c:4751)
  vm_exec_core (insns.def:759)
  rb_vm_exec (vm.c:2211)
  invoke_block (vm.c:1316)
  invoke_iseq_block_from_c (vm.c:1372)
  invoke_block_from_c_bh (vm.c:1390)
  vm_yield_with_cref (vm.c:1427)
  vm_yield (vm.c:1435)
  rb_yield_0 (vm_eval.c:1347)
  rb_yield (vm_eval.c:1363)
  rb_ary_each (array.c:2522)
  vm_call_cfunc_with_frame (vm_insnhelper.c:3037)
  vm_sendish (vm_insnhelper.c:4751)
  vm_exec_core (insns.def:759)
  rb_vm_exec (vm.c:2211)
  invoke_block (vm.c:1316)
  invoke_iseq_block_from_c (vm.c:1372)
  invoke_block_from_c_bh (vm.c:1390)
  vm_yield_with_cref (vm.c:1427)
  vm_yield (vm.c:1435)
  rb_yield_0 (vm_eval.c:1347)
  rb_yield (vm_eval.c:1363)
  rb_ary_each (array.c:2522)
  vm_call_cfunc_with_frame (vm_insnhelper.c:3037)
  vm_sendish (vm_insnhelper.c:4751)
  vm_exec_core (insns.def:759)
  rb_vm_exec (vm.c:2211)
  invoke_block (vm.c:1316)
  invoke_iseq_block_from_c (vm.c:1372)
  invoke_block_from_c_bh (vm.c:1390)
  vm_yield_with_cref (vm.c:1427)
  vm_yield (vm.c:1435)
  rb_yield_0 (vm_eval.c:1347)
  rb_yield (vm_eval.c:1363)
  rb_ary_collect (array.c:3564)
  vm_call_cfunc_with_frame (vm_insnhelper.c:3037)
  vm_sendish (vm_insnhelper.c:4751)
  vm_exec_core (insns.def:759)
  rb_vm_exec (vm.c:2211)
  rb_vm_invoke_proc (vm.c:1521)
  rb_proc_call_kw (proc.c:995)
  exec_end_procs_chain (eval_jump.c:105)
  rb_ec_exec_end_proc (eval_jump.c:120)
  rb_ec_teardown (eval.c:155)
  rb_ec_cleanup (eval.c:205)
  ruby_run_node (eval.c:321)
  main (main.c:47)

rake aborted!
Valgrind reported errors (e.g. memory leak or use-after-free)
/usr/local/rvm/gems/ruby-3.1.4/gems/ruby_memcheck-1.3.2/lib/ruby_memcheck/test_task_reporter.rb:19:in `report_valgrind_errors'

I'm not super familiar with what it is telling us about malloc here, so I wanted to report it with the hope that someone will know what to look at.

Recommendations for conditional boolean attributes?

I am curious how do people write boolean attributes with the existing recommendations? Is there a way to write them in a way that is parsed into the AST?

For something like this

<button <% unless editable? %>disabled<% end %>>Edit</button>

it gives us attribute name disabled<% end %> with empty attribute value.

And html_attributes

<button <%= html_attributes(disabled: editable?) %>>Edit</button>
<button <%= html_attributes(disabled: true) if editable? %>>Edit</button>

doesn't actually gets parsed as attributes.

This markup doesn't work for boolean attributes for obvious reasons.

<button disabled="<%= editable? %>"></button>

🤔

Thanks!

not working the partial_attribute_name_pattern setting.

I'm setting up with reference to README.md,
https://github.com/Shopify/better-html#how-to-use

but when I run the test with RSpec, the following error is displayed.

      ActionView::Template::Error:
        Invalid attribute name "class" does not match regular expression "/\\A[a-zA-Z0-9\\-\\:\\_]+\\z/"
        On line 1 column 5:
        <div class="container-fluid" id="wrapper_container">

I've tried several things with reference to the code in the gem's configuration file, but it doesn't work.
https://github.com/Shopify/better-html/blob/d2e117d6e6d375edfc25a664b1d04e1db7d4931f/lib/better_html/config.rb

When checking the operation of regular expressions using Rubular, my notation seems to work without problems.
10_01_14

I develop with Docker.
OS: mac, BigSur
Ruby: 2.7.7
Rails: 6.1.7
erb_lint: 0.3.1
better_html: 2.0.1

My code is here.

Gemfile

source 'https://rubygems.org'

git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.7.7'

group :development, :test do
  gem "better_html"
  gem 'erb_lint', require: false
end

config/initializers/better_html.rb

BetterHtml.config = BetterHtml::Config.new(YAML.load(File.read("#{Rails.root}/.better-html.yml")))

.better-html

---
allow_single_quoted_attributes: true
allow_unquoted_attributes: false
partial_attribute_name_pattern: /\A[a-zA-Z0-9\-\:\_]+\z/
# pending
# partial_tag_name_pattern: /\A[a-z0-9\-\:\_]+\z/

Please let me know why it doesn't work and how to fix it.
thank you.

Rails 6.0.0. alpha - Deprecation warning

Receiving this notice in the Rails 6 alpha log:

DEPRECATION WARNING: escape_whitelist is deprecated and will be removed from Rails 6.1 (use #escape_ignore_list instead) (called from generate at /Users/user/.rvm/gems/ruby-2.6.0@ruby260rails6alpha/gems/better_html-1.0.11/lib/better_html/better_erb.rb:61)

Rails 6.0.0 alpha, Ruby 2.6.0

Prevent validating rails templates

Problem

Templates outside the app structure (ex: rails) are being validated.

Symptoms:

  • The email preview templates in rails that surround preview emails rails/mailers raise BetterHtml::DontInterpolateHere in Rails::Mailers#preview

Solution

TBD

ERB parser crashes on multiple CDATA blocks

Reproduce with:

require 'better_html'
require 'better_html/parser'
b = Parser::Source::Buffer.new('(buffer)');
b.source = '<![CDATA[foo]]><![CDATA[bar]]>'
BetterHtml::Parser.new(b).inspect

Result:

RuntimeError: Unhandled token cdata_end line 1 column 27, [s(:cdata, "foo"), s(:text, "<![CDATA[bar")]
from better_html-1.0.16/lib/better_html/parser.rb:79:in `build_document_node'

The problem is in html_tokenizer and a PR with a fix available: https://github.com/EiNSTeiN-/html_tokenizer/pull/7

html attributes helper for value-less attributes

So I'm aware that you can do html_attributes("data-value": nil) if condition? to return "data-value" if the condition is truthy.

Is that the cleanest / only format? On reading the code, I briefly thought html_attributes("data-value") if condition? might work, but I overlooked the stringify_keys statement that means it needs to be a hash.

Would a PR to support strings, or arrays of strings, in the method be considered?

Or is this more a case of we've provided a basic clean interface to prevent too much magic producing bad HTML, so the preference is not sprinkle too much interface overloading magic in?!

Add support to configure Parser

This is stemming from an issue running erb-lint on files with AlpineJS syntax (Shopify/erb-lint#221), but it would be nice to add support to configure the parser to use some of the same configuration that better-html supports (in this case the partial_attribute_name_pattern would correct this). For example, being able to set config.partial_attribute_name_pattern = /\A[a-zA-Z0-9\-\:\@\.]+\z/ would allow for the following snippet to parse correctly:

<nav x-data="{ open: false }" @keydown.window.escape="open = false" class="bg-white border-b"></nav>

It looks like this might be a bit tricky given the underlying issue comes from the underlying html_tokenizer library.

Add support for annotate_template_file_names

Rails 6.1 recently introduce the configuration config.action_view.annotate_template_file_names in this PR: rails/rails#38848

I'm not able to see the comments in the HTML until i've removed the better_html gem.

Could you add the support for this new configuration?

Partial incompatibility with rails 7.1

I realised after upgrading to rails 7.1 that when I encounter an exception in the view, I do not get the familiar ActionDispatch::DebugExceptions page anymore, but rather my exceptions_app

After much digging, I realised there is a crash inside ActionDispatch::DebugExceptions when trying to display the backtrace, thus making the middleware not render the page, and the exception ends up getting caught by ActionDispatch::ShowExceptions down the road.

Here's the exception and the stacktrace :

eval error: undefined method `last' for nil:NilClass
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionview-7.1.1/lib/action_view/template/handlers/erb.rb:142:in `find_offset'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionview-7.1.1/lib/action_view/template/handlers/erb.rb:46:in `translate_location'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionview-7.1.1/lib/action_view/template.rb:229:in `translate_location'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/exception_wrapper.rb:258:in `spot'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/exception_wrapper.rb:302:in `extract_source'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/exception_wrapper.rb:206:in `block in source_extracts'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/exception_wrapper.rb:205:in `map'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/exception_wrapper.rb:205:in `source_extracts'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/debug_exceptions.rb:126:in `create_template'
/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/debug_exceptions.rb:78:in `render_for_browser_request'
  (rdbg)/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.1/lib/action_dispatch/middleware/debug_exceptions.rb:1:in `rescue in call'

better_html sounded like a good suspect, removing it from the bundle fixes the issue.

Alternatively, disabling runtime checks fixes the issue too :

BetterHtml.configure do |config|
  config.template_exclusion_filter = proc { true }
end

So it seems that the custom parser conflicts with the way rails 7.1 tries to extract the source.

Rails 6.0 incompatible: `annotate_rendered_view_with_filenames`

The method annotate_rendered_view_with_filenames is Rails 6.1+ only.

BetterHtml.config.annotate_rendered_view_with_filenames = ActionView::Base.annotate_rendered_view_with_filenames

I found this issue while running the test for maintenance_tasks https://github.com/Shopify/maintenance_tasks/actions/runs/8289887525/job/22695983684?pr=991

rails aborted!
NoMethodError: undefined method `annotate_rendered_view_with_filenames' for ActionView::Base:Class
/home/runner/work/maintenance_tasks/maintenance_tasks/vendor/bundle/ruby/3.0.0/gems/better_html-2.1.0/lib/better_html/railtie.rb:11:in `block (2 levels) in <class:Railtie>'

Plans to support RBS?

I was curious if the team is planning on adding (or accepting a pull request to add) a sig directory with RBS files or something along those lines.

html_attributes for adding extra values?

Is there something available in better html to make this line more elegant / DRY, whilst still passing the default ERB compilation checks?

<div
   <%= some_condition? ? html_attributes(class: "foo bar ") : html_attributes(class: "foo") %>>
</div>

Note - previously this code was

<div class="foo <% if some_condition? %>bar<% end %>">

Obviously this didn't pass the checks!

Unavoidable error on inline SVG element <clipPath>

I just installed better-html on https://github.com/Shopify/downrigger but I'm getting two errors (the same error from each recommended test:

ActionView::Template::Error: Invalid tag name "clipPath" does not match regular expression /\A[a-z0-9\-\:]+\z/
On line 129 column 8:
                                                        <clipPath id="border-clip">
        ^^^^^^^^
    test/controllers/ui_controller_test.rb:6:in `block in <class:UiControllerTest>'

https://github.com/Shopify/downrigger/blob/master/app/views/ui/show.html.erb#L129

I get why it's throwing the error, but other than this test I have no reason to not have this as inline image code, since it allows for better CSS manipulation, which is a key aspect to the design.

The tag itself is to spec:

https://www.w3.org/TR/SVG11/masking.html#EstablishingANewClippingPath

I'm unfamiliar with better-html, are there any ignore rules I can put in place anywhere?

Replace "ast" gem with custom implementation

The "ast" gem, a core dependency of better-html, is no longer actively maintained, with its most recent non-trivial commit happening nearly 3 years ago. Having crucial functionality of the gem be dependent on "ast" is unsustainable at best and dangerous at worst. Therefore, better-html should ideally move towards making its own AST class. It frankly does not utilize much of the features from the "ast" gem anyways, so writing a tailormade AST class for better-html should not be too challenging.

Tasks

No tasks being tracked yet.

Wow so cool :)

Thanks for making it and sharing.

Quick question: why would I use that and not Slim? (or Haml, etc)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.