The Dev Blog

Putting Family Management on Rails!

Lost In Binding - Adventures In Ruby Metaprogramming

Posted by Guy Naor Wed, 28 Mar 2007 07:00:00 GMT

I've been using the security_extensions plugin to secure forms in Famundo and some other projects. It's a very simple plugin that adds protection against CSRF.

When upgrading one of my projects to Rails 1.2, I got a deprecation warnings from Rails, as this plugin requires start_form_tag and end_form to work. Thinking it was all easy to change, I replaced all calls to the new form_tag ... do format. This change resulted in lots of errors, all complains from erb on missing the _erbout variable.

After a lot of digging, I realized this error is caused by the way variables bindings work in Ruby, and the fact that erb uses it to pass along the output string it creates.

What are bindings? Bindings are (put very simply) the context for the variables in an execution block. It's what's used in Ruby to bind a variable to a block and have the block access it even after the variable went out of scope in the original code block, or the variable is re-defined in a new block. Here is some code to make it clear:

# Define the a var
a = 5
# Create a proc that prints a
level_a = lambda { puts a }
level_a.call # => 5

# Create a method that accepts a proc, redefines a and calls the proc
def level_b(blk)
  a = 10
  blk.call
end

level_b(level_a) # => 5  

The best place I found to learn about bindings is this page.

How is this affecting the security_extensions plugin? The plugin needs to wrap the block given to the form, and inject into it the hidden field used to validate the form when it's posted back. When using the non-block accepting start_form_tag, it just appends a new field at the end:

def secure_form_tag(*args)
  return start_form_tag(*args) + "\n" +
    hidden_field_tag('session_id_validation', security_token)
end

The simple solution I thought will work, is very simple:

def secure_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
  if block_given?
    form_tag(url_for_options, options, *parameters_for_url) do
      yield
    end
    hidden_field_tag("session_id_validation", security_token)
  else
    "#{form_tag(*args)} \n #{hidden_field_tag('session_id_validation', security_token)}"
  end
end

It failed. I decided to try and call the external block directly:

def secure_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
  if block_given?
    form_tag(url_for_options, options, *parameters_for_url) do
      block.call
    end
    hidden_field_tag("session_id_validation", security_token)
  else
    "#{form_tag(*args)} \n #{hidden_field_tag('session_id_validation', security_token)}"
  end
end

Failed again! The problem is that the context of the internal block is completely different from the context of the block passed to the function, and so the _erbout variable isn't bound to the internal block, only to the external one.

The solution I used was to copy the binding from the passed block into the internal block, call the passed block, and then copy it back from the internal block to the passed block. Here is the code to do it:

def secure_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
  if block_given?
    _erbout = eval('_erbout', block)
    form_tag(url_for_options, options, *parameters_for_url) do
      concat(hidden_field_tag("session_id_validation", security_token), block.binding)
      eval "_erbout = %q[#{_erbout}]"
      yield
    end
    eval "_erbout = %q[#{_erbout}]", block
  else
    "#{form_tag(*args)} \n #{hidden_field_tag('session_id_validation', security_token)}"
  end
end

This code does a lot of copying of strings, but as it's in very specific places that aren't performance sensitive, I rather get the nice way to use secured forms, and incur the performance penalty in this case.

The same trick can be use to extended other erb related methods that use blocks and need the _erbout bindings.

If there are better solutions, let me know. I'd love a simpler solution for this.

Posted in ,  | 7 comments

del.icio.us:Lost In Binding - Adventures In Ruby Metaprogramming digg:Lost In Binding - Adventures In Ruby Metaprogramming spurl:Lost In Binding - Adventures In Ruby Metaprogramming wists:Lost In Binding - Adventures In Ruby Metaprogramming simpy:Lost In Binding - Adventures In Ruby Metaprogramming newsvine:Lost In Binding - Adventures In Ruby Metaprogramming blinklist:Lost In Binding - Adventures In Ruby Metaprogramming furl:Lost In Binding - Adventures In Ruby Metaprogramming reddit:Lost In Binding - Adventures In Ruby Metaprogramming fark:Lost In Binding - Adventures In Ruby Metaprogramming blogmarks:Lost In Binding - Adventures In Ruby Metaprogramming Y!:Lost In Binding - Adventures In Ruby Metaprogramming smarking:Lost In Binding - Adventures In Ruby Metaprogramming magnolia:Lost In Binding - Adventures In Ruby Metaprogramming segnalo:Lost In Binding - Adventures In Ruby Metaprogramming

Comments

  1. K. Adam Christensen said 1 day later:

    What if you just did a concat on the hidden tag and the block's binding and then pass that block into the form_for method.

    http://pastie.caboo.se/50396

    Not tested, but an idea.

  2. Piers Cawley said 2 days later:

    I've not tried it, but what's wrong with:

    form_tag(url_for_options, *parameters_for_url, &block)
    

    If you don't create a new block, you don't have to create a new binding.

  3. Guy Naor said 2 days later:

    Piers,

    Unfortunately this can't work. I wish it was that simple :-).

    The reason it doesn't, is that when using

    form_tag(...) do end
    the form and /form tags wrap the form elements around the block. And so our added hidden field will be outside the form. We need a way to inject the hidden field into the block.

  4. Piers Cawley said 3 days later:

    Ah yes, of course.

    Further inspection of the docs suggests that:

    def security_token_field
      hidden_field_tag "session_id_validation"), security_token
    end
    
    def concatenator(binding)
      lambda {|str| concat(str, binding)}
    end
    
    def secure_form_tag(*form_tag_params, &block)
      if block_given?
        form_body = capture █
        concatenator(block).call \
          form_tag(*form_tag_params) { security_token_field + form_body }
      else
        "#{form_tag(*form_tag_params)}
    #{hidden_field_tag('session_id_validation', security_token)}"
      end
    end

    Should do the trick. All that ugliness with string_eval still happens, but it's hidden behind the abstraction wall.

    I may have got a little bit Higher Order Function happy on concatenate's ass.

  5. Piers Cawley said 3 days later:

    Dang, forgot to replace both calls to hidden_field_tag in that rewritten secure_form_tag. L'esprit d'escalier strikes again.

  6. Guy Naor said 4 days later:

    Piers,

    This is nice, though like you said, not much different with regard to all the copying of the strings, which I suspect can't be prevented.

    Thanks for the idea!

  7. Guy Naor said 4 days later:

    Piers, unfortunately the code you posted still doesn't work. The reason it doesn't is that we still have an internal block in the call to form_tag that goes into the concatenator.

    But I did find a way of using capture which simplifies the code (though still makes it look a bit hackish...):

    def secure_form_tag(*form_tag_params, &block)
      if block_given?
        @res = <<-EOF
          #{form_tag(*form_tag_params)}
          #{capture(&block)}
          #{hidden_field_tag('session_id_validation', security_token)}
          </form>
        EOF
        eval '_erbout.concat @res', block
      else
        "#{form_tag(*form_tag_params)} #{hidden_field_tag('session_id_validation', security_token)}"
      end
    end

    BTW, notice the need to use @res to make it work, or the var is hidden from the block bindings.

    Thanks again for the capture trick, didn't see it before :-)

Comments are disabled

Subscribe to The Dev Blog