├── ISSUES ├── VERSION ├── .document ├── lib ├── owasp-esapi-ruby.rb ├── executor.rb ├── file_scanner.rb ├── intrustion_detector.rb ├── exceptions.rb ├── codec │ ├── oracle_codec.rb │ ├── percent_codec.rb │ ├── vbscript_codec.rb │ ├── os_codec.rb │ ├── base_codec.rb │ ├── pushable_string.rb │ ├── javascript_codec.rb │ ├── mysql_codec.rb │ ├── xml_codec.rb │ ├── css_codec.rb │ ├── html_codec.rb │ └── encoder.rb ├── validator │ ├── validator_error_list.rb │ ├── integer_rule.rb │ ├── html_rule.rb │ ├── float_rule.rb │ ├── base_rule.rb │ ├── date_rule.rb │ └── string_rule.rb ├── esapi.rb └── validator.rb ├── AUTHORS ├── spec ├── owasp_esapi_executor_spec.rb ├── spec_helper.rb ├── codec │ ├── oracle_codec_spec.rb │ ├── vbcript_codec_spec.rb │ ├── percent_codec_spec.rb │ ├── mysql_codec_spec.rb │ ├── javascript_codec_spec.rb │ ├── css_codec_spec.rb │ ├── os_codec_spec.rb │ ├── html_codec_spec.rb │ └── xml_codec_spec.rb ├── validator │ ├── base_rule_spec.rb │ ├── float_rule_spec.rb │ ├── date_rule_spec.rb │ ├── html_rule_spec.rb │ ├── integer_rule_spec.rb │ └── string_rule_spec.rb ├── antisamy-esapi.xml ├── owasp_esapi_encoder_spec.rb └── owasp_esapi_validator_spec.rb ├── .gitignore ├── LICENSE ├── Rakefile ├── README ├── owasp-esapi-ruby.gemspec └── ChangeLog /ISSUES: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.30.0 -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /lib/owasp-esapi-ruby.rb: -------------------------------------------------------------------------------- 1 | require 'esapi' 2 | require 'exceptions' 3 | require 'codec/encoder' 4 | require 'validator' 5 | require 'executor' -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Owasp Esapi Ruby core 2 | --------------------- 3 | 4 | * Paolo Perego 5 | * Sal Scotto -------------------------------------------------------------------------------- /spec/owasp_esapi_executor_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | describe Executor do 6 | it "execute a command and get output" 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | _site 23 | *.pxm 24 | .yardoc/ 25 | doc/ 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | 4 | require 'owasp-esapi-ruby' 5 | require 'rspec' 6 | require 'rspec/autorun' 7 | 8 | Owasp::Esapi.security_config.resources["antisamy"] = "#{File.dirname(__FILE__)}/antisamy-esapi.xml" 9 | 10 | RSpec.configure do |config| 11 | config.color_enabled = true 12 | end 13 | -------------------------------------------------------------------------------- /lib/executor.rb: -------------------------------------------------------------------------------- 1 | # Executor implmentation 2 | # 3 | # Provide a safe execute command, that wll ensure paths and args are escaped properly 4 | # and check for expansions of the command 5 | # 6 | 7 | module Owasp 8 | module Esapi 9 | # Executor class 10 | class Executor 11 | 12 | # Wrapper for Process#spawn 13 | # it sanitizes the parames and validates paths before execution 14 | def execute_command(cmd,params,working_dir,codec,redirect_error) 15 | cmd_path = File.expand_path(cmd) 16 | 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/file_scanner.rb: -------------------------------------------------------------------------------- 1 | module Owasp 2 | module Esapi 3 | # FileScanner 4 | # This is class is unique to Esapi for Ruby 5 | # Users should developer thier own scanner class and configure esapi with the implmentation 6 | # The purpose of this class is to scan a file for encoding/viruses/etc 7 | # child classes only need to implment the scan method of the class 8 | class FileScanner 9 | # scan an IO object for viruses, encoding requirements, etc.. 10 | # raise an Exception if there are any issues found within the IO stream 11 | # callers will be expected to rewind the stream 12 | def scan(io_object) 13 | end 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /spec/codec/oracle_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe OracleCodec do 7 | let (:codec) { Owasp::Esapi::Codec::OracleCodec.new } 8 | 9 | it "should encode eddie's stuff as eddie''s stuff" do 10 | codec.encode([],"eddie's stuff").should == "eddie''s stuff" 11 | end 12 | it "should encode \' as \'\'" do 13 | codec.encode([],"\'").should == "\'\'" 14 | end 15 | 16 | it "should decode \'\' as \'" do 17 | codec.decode("\'\'").should == "\'" 18 | end 19 | 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/codec/vbcript_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe VbScriptCodec do 7 | let (:codec) { Owasp::Esapi::Codec::VbScriptCodec.new } 8 | it "should encode < as chrw(60)" do 9 | codec.encode([],"<").should == "chrw(60)" 10 | end 11 | it "should encode 0x100 as \\u0100" do 12 | s = 0x100.chr(Encoding::UTF_8) 13 | codec.encode([],s[0]).should == "chrw(256)" 14 | end 15 | 16 | it "should decode '\"<' as <" do 17 | codec.decode("\"<").should == "<" 18 | end 19 | 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/validator/base_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Validator 6 | describe BaseRule do 7 | let(:rule) {Owasp::Esapi::Validator::BaseRule.new("test")} 8 | it "should remove non whitelist characters" do 9 | rule.whitelist("12345abcdefghijkmlaaaa","abc").should == "abcaaaa" 10 | end 11 | 12 | it "should raise and exception in the base class" do 13 | lambda {rule.valid("test","input")}.should raise_error(Owasp::Esapi::ValidationException) 14 | end 15 | 16 | it "should return false for valid? int eh base rule" do 17 | rule.valid?("test","input").should be_false 18 | end 19 | 20 | it "should has an item in the error list" do 21 | v = Owasp::Esapi::Validator::ValidatorErrorList.new 22 | rule.validate("context","input",v) 23 | v.errors.should_not be_empty 24 | end 25 | 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/codec/percent_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | # percent encode aka URL encoding 4 | module Owasp 5 | module Esapi 6 | module Codec 7 | describe PercentCodec do 8 | let (:codec) { Owasp::Esapi::Codec::PercentCodec.new } 9 | 10 | it "should decode %3c as <" do 11 | codec.decode("%3c").should == "<" 12 | end 13 | 14 | it "should encode < as %3C" do 15 | codec.encode([],"<").should == "%3C" 16 | end 17 | 18 | it "should encode 0x100 as %C4%80" do 19 | s = 0x100.chr(Encoding::UTF_8) 20 | codec.encode([],s[0]).should == "%C4%80" 21 | end 22 | 23 | it "should decode %25F as %F" do 24 | codec.decode("%25F").should == "%F" 25 | end 26 | 27 | it "should encode 'Stop!' said Fred as %27Stop%21%27+said+Fred" do 28 | codec.encode([],"'Stop!' said Fred").should == "%27Stop%21%27+said+Fred" 29 | end 30 | 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/validator/float_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Validator 6 | describe FloatRule do 7 | 8 | it "should validate 4.3214 as valid within range of -10 to 10" do 9 | rule = Owasp::Esapi::Validator::FloatRule.new("test",nil,-10,10) 10 | rule.valid?("","4.3214").should be_true 11 | end 12 | 13 | it "should fail to validate -1 for range of 0 to 100" do 14 | rule = Owasp::Esapi::Validator::FloatRule.new("test",nil,0,100) 15 | rule.valid?("","-1").should be_false 16 | end 17 | 18 | it "should not validate 1e-6 as valid within range of -999999999 to 999999999" do 19 | rule = Owasp::Esapi::Validator::FloatRule.new("test",nil,-999999999,999999999) 20 | rule.valid?("","1e-6").should be_true 21 | end 22 | 23 | it "should raise an error when a non string is passed in" do 24 | rule = Owasp::Esapi::Validator::FloatRule.new("test",nil,0,300) 25 | lambda{ rule.valid("","#{Float::INFINITY}") }.should raise_error(ValidationException) 26 | end 27 | 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/intrustion_detector.rb: -------------------------------------------------------------------------------- 1 | module Owasp 2 | module Esapi 3 | class IntrustionDetector 4 | 5 | def add_exception(exception) 6 | return unless Esapi.security_config.ids? 7 | if exception.is_a?(EnterpriseSecurityException) 8 | # log a security failure warning, with th log message and exception 9 | else 10 | # log a security failure warning with the exception message 11 | end 12 | 13 | # Add exception to current user 14 | 15 | end 16 | 17 | def add_event(event,message) 18 | return unless Esapi.security_config.ids? 19 | 20 | end 21 | 22 | end 23 | private 24 | class IntrustionEvent 25 | def initialize(key) 26 | @key = key 27 | @times = [] 28 | end 29 | def increment(count,interval) 30 | return unless Esapi.security_config.ids? 31 | 32 | now = Time.now 33 | @times.unshift(now) 34 | if @times.size > count 35 | @times.slice!(count,@times.size - count) 36 | end 37 | if @times.size == count 38 | past = @times.last 39 | if now - past < (interval * 1000) 40 | raise IntrustionException.new("Threshold exceeded","Exceeded threshold for #{key}") 41 | end 42 | end 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/validator/date_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Validator 6 | describe DateRule do 7 | let(:rule) {Owasp::Esapi::Validator::DateRule.new("test",nil,nil)} 8 | 9 | it "should validate September 11, 2001 as a valid" do 10 | rule.valid?("","September 11, 2001").should be_true 11 | end 12 | 13 | it "should fail to validate 9-11-2001 as valid with the default format" do 14 | rule.valid?("","9-11-2001").should be_false 15 | end 16 | 17 | it "should fail to validate with a null date" do 18 | rule.valid?("",nil).should be_false 19 | end 20 | 21 | it "should fail to validate with an empty string as the date" do 22 | rule.valid?("","").should be_false 23 | end 24 | 25 | # Try a few different date formats 26 | { 27 | "Jan 1, 07 Sun GMT" => "%b %d, %y %Z", 28 | "31-12-2010" => "%d-%m-%Y", 29 | "31-1-2010" => "%d-%m-%Y", 30 | "2010-02-27 15:00" => "%Y-%m-%d %H:%M" 31 | }.each_pair do |k,v| 32 | it "should validate #{k} as a valid date with #{v} as the format" do 33 | rule = Owasp::Esapi::Validator::DateRule.new("test",nil,v) 34 | rule.valid?("",k).should be_true 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/exceptions.rb: -------------------------------------------------------------------------------- 1 | # Various exception used by Esapi 2 | module Owasp 3 | module Esapi 4 | 5 | # Base Exception class for SecurityExceptions 6 | class EnterpriseSecurityException < Exception 7 | attr_reader :log_message, :cause 8 | def initialize(user_msg, log_msg,cause) 9 | super(user_msg) 10 | @log_message = log_msg 11 | @cause = cause 12 | # If we have esapi configured for IDS, pass the exception to ids 13 | end 14 | end 15 | 16 | # Exception throw if there is an error during Executor processing 17 | class ExecutorException < EnterpriseSecurityException 18 | end 19 | 20 | # Intrustion detection exception to be logged 21 | class IntrustionException < EnterpriseSecurityException 22 | def initialize(user_message,log_message) 23 | super(user_message,log_message,nil) 24 | end 25 | end 26 | 27 | # ValidatorException used in the rule sets 28 | class ValidationException < EnterpriseSecurityException 29 | attr_reader :context 30 | def initialize(user_msg,log_msg,context, cause=nil) 31 | super(user_msg,log_msg,cause) 32 | @context = context 33 | end 34 | end 35 | 36 | # Configuration exception 37 | class ConfigurationException < Exception 38 | attr_reader :cause 39 | def initialize(msg,error) 40 | super(msg) 41 | @cause = error 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/codec/oracle_codec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Codec to provide for Oracle string support 3 | # see http://oraqa.com/2006/03/20/how-to-escape-single-quotes-in-strings for details 4 | # This will only prevent SQLinjection in the case of user data being placed within an 5 | # Oracle quoted string such as select * from table where field = ' USERDATA ' 6 | module Owasp 7 | module Esapi 8 | module Codec 9 | class OracleCodec < BaseCodec 10 | 11 | # Encodes ' to '' 12 | def encode_char(immune,input) 13 | return "\'\'" if input == "\'" 14 | input 15 | end 16 | 17 | # Returns the decoded version of the character starting at index, or 18 | # nil if no decoding is possible. 19 | # 20 | # Formats all are legal 21 | # '' decodes to ' 22 | def decode_char(input) 23 | # check first *char* 24 | input.mark 25 | first = input.next 26 | if first.nil? 27 | input.reset 28 | return nil 29 | end 30 | # if it isnt an encoded string return nil 31 | unless first == "\'" 32 | input.reset 33 | return nil 34 | end 35 | # if second isnt an encoded marker return nil 36 | second = input.next 37 | unless second == "\'" 38 | input.reset 39 | return nil 40 | end 41 | return "\'" 42 | end 43 | end 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/validator/validator_error_list.rb: -------------------------------------------------------------------------------- 1 | module Owasp 2 | module Esapi 3 | module Validator 4 | # List of Validation exceptions 5 | # this list is indexed by the context 6 | class ValidatorErrorList 7 | 8 | # Create a new list 9 | def initialize() 10 | @errors = {} 11 | end 12 | 13 | # Add an error to the list. We will raise ArgumentException if any of the following is true: 14 | # 1. error is nil 15 | # 2. context is nil 16 | # 3. we already have an error for the given context 17 | # 4. the error isnt a ValidationException 18 | def <<(error) 19 | raise ArgumentError.new("Invalid Error") if error.nil? 20 | if error.instance_of?(ValidationException) 21 | context = error.context 22 | raise ArgumentError.new("Invalid context") if context.nil? 23 | raise ArgumentError.new("Duplicate error") if @errors.has_key?(context) 24 | @errors[context] = error 25 | else 26 | raise ArgumentError.new("Exception was not a ValdiaitonException") 27 | end 28 | end 29 | 30 | # Return true if this list is empty 31 | def empty? 32 | @errors.empty? 33 | end 34 | 35 | # Return the size of the list 36 | def size 37 | @errors.size 38 | end 39 | 40 | # Return the array of errors in this list 41 | def errors 42 | @errors.values 43 | end 44 | 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011, The OWASP Foundation 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /spec/codec/mysql_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe MySQLCodec do 7 | let (:ansi_codec) { Owasp::Esapi::Codec::MySQLCodec.new(Owasp::Esapi::Codec::MySQLCodec::ANSI_MODE) } 8 | let (:mysql_codec) { Owasp::Esapi::Codec::MySQLCodec.new(Owasp::Esapi::Codec::MySQLCodec::MYSQL_MODE) } 9 | let (:big_char) { } 10 | 11 | it "should encode \' as \'\' in ANSI mode" do 12 | ansi_codec.encode([],"\'").should == "\'\'" 13 | end 14 | 15 | it "should encode < as \\< in MYSQL mode" do 16 | mysql_codec.encode([],"<").should == "\\<" 17 | end 18 | 19 | it "should encode 0x100 as \\0x100 in MYSQL mode" do 20 | s = 0x100.chr(Encoding::UTF_8)[0] 21 | mysql_codec.encode([],s) == "\\#{s}" 22 | end 23 | 24 | it "should encode 0x100 as 0x100 in ANSI mode" do 25 | s = 0x100.chr(Encoding::UTF_8)[0] 26 | ansi_codec.encode([],s) == "#{s}" 27 | end 28 | 29 | it "should decode '' as ' in ANSI mode" do 30 | ansi_codec.decode("\'\'").should == "\'" 31 | end 32 | 33 | it "should decode \\< as < in MYSQL mode" do 34 | mysql_codec.decode("\\<").should == "<" 35 | end 36 | 37 | it "should fail to create a code with an invalid mode" do 38 | lambda { Owasp::Esapi::Codec::MySQLCodec.new(5)}.should raise_error(RangeError) 39 | end 40 | 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/codec/percent_codec.rb: -------------------------------------------------------------------------------- 1 | # Implementation of the Codec interface for percent encoding (aka URL encoding). 2 | module Owasp 3 | module Esapi 4 | module Codec 5 | class PercentCodec < BaseCodec 6 | 7 | # Encode a character for URLs 8 | def encode_char(immune,input) 9 | return input if input =~ /[a-zA-Z0-9_.-]/ 10 | # RFC compliance 11 | return "+" if input == " " 12 | val = '' 13 | input.each_byte do |b| 14 | val << '%' << b.ord.to_h.upcase 15 | end 16 | val 17 | end 18 | 19 | # Formats all are legal both upper/lower case: 20 | # %hh; 21 | def decode_char(input) 22 | input.mark 23 | first = input.next 24 | if first.nil? 25 | input.reset 26 | return nil 27 | end 28 | # check if this is an encoded character 29 | if first != '%' 30 | input.reset 31 | return nil 32 | end 33 | # search for 2 hex digits 34 | tmp = '' 35 | for i in 0..1 do 36 | c = input.next_hex 37 | tmp << c unless c.nil? 38 | end 39 | # we found 2, convert to a number 40 | if tmp.size == 2 41 | i = tmp.hex 42 | begin 43 | return i.chr(Encoding::UTF_8) if i >= START_CODE_POINT and i <= END_CODE_POINT 44 | rescue Exception => e 45 | end 46 | end 47 | input.reset 48 | nil 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/codec/javascript_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe JavascriptCodec do 7 | let (:codec) { Owasp::Esapi::Codec::JavascriptCodec.new } 8 | 9 | it "should decode \\x3c as <" do 10 | codec.decode("\\x3c").should == "<" 11 | end 12 | 13 | it "should encode < as \\x3C" do 14 | codec.encode([],"<").should == "\\x3C" 15 | end 16 | 17 | it "should encode 0x100 as \\u0100" do 18 | s = 0x100.chr(Encoding::UTF_8) 19 | codec.encode([],s[0]).should == "\\u0100" 20 | end 21 | 22 | it "should encode ").should == "Test." 11 | end 12 | 13 | it "should string bold tags off of jeff" do 14 | rule.valid("test","Jeff").should == "Jeff" 15 | end 16 | it "should leave the link alone according to the test rules" do 17 | input = "Aspect Security" 18 | rule.valid("test",input).should == input 19 | end 20 | 21 | it "should remove the escaped div" do 22 | input = "Test. <
load=alert()" 23 | rule.valid("test",input).should == "Test. load=alert()" 24 | end 25 | 26 | it "should clean out the expression" do 27 | input = "Test.
b
" 28 | rule.valid("test",input).should == "

Test.

b
" 29 | end 30 | 31 | it "should clean out the script" do 32 | input = "Test. alert(document.cookie)" 33 | rule.valid("test",input).should == "Test." 34 | end 35 | 36 | it "should remove the escaped script" do 37 | input = "Test. alert(document.cookie)" 38 | rule.valid("test",input).should == "Test. alert(document.cookie)" 39 | end 40 | 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/codec/css_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe CssCodec do 7 | let (:codec) { Owasp::Esapi::Codec::CssCodec.new } 8 | 9 | it "should encode my '<' as \\3c" do 10 | m = codec.encode([],"<") 11 | m.should == '\\3c ' 12 | end 13 | 14 | it "should decode \\abcdefg and replace the invliad code point" do 15 | s = "\\abcdefg" 16 | codec.decode(s).should == "\uFFFDg" 17 | end 18 | it "should encode 0x100 as \\100" do 19 | s = 0x100.chr(Encoding::UTF_8) 20 | m = codec.encode([],s[0]) 21 | m.should == "\\100 " 22 | end 23 | 24 | it "should decode '\\<' to '<'" do 25 | m = codec.decode("\\<") 26 | m.should == "<" 27 | end 28 | 29 | it "should decode '\\41xyz' to Axyz" do 30 | m = codec.decode("\\41xyz") 31 | m.should == "Axyz" 32 | end 33 | 34 | it "should decode '\\000041abc' to 'Aabc'" do 35 | m = codec.decode("\\000041abc") 36 | m.should == "Aabc" 37 | end 38 | 39 | it "should decode '\\41 abc' to 'Aabc'" do 40 | m = codec.decode("\\41 abc") 41 | m.should == "Aabc" 42 | end 43 | 44 | it "should decode 'abc\\\nxyz' to 'abcxyz'" do 45 | m = codec.decode("abc\\\nxyz") 46 | m.should == "abcxyz" 47 | end 48 | 49 | it "should decode 'abc\\\r\nxyz' to 'abcxyz'" do 50 | m = codec.decode("abc\\\r\nxyz") 51 | m.should == "abcxyz" 52 | end 53 | 54 | it "should decode \\3c as <" do 55 | codec.decode("\\3c").should == "<" 56 | end 57 | 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/codec/os_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe OsCodec do 7 | let(:unix_codec) {Owasp::Esapi::Codec::OsCodec.new( Owasp::Esapi::Codec::OsCodec::UNIX_HOST)} 8 | let(:win_codec) {Owasp::Esapi::Codec::OsCodec.new( Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST)} 9 | 10 | it "should detect the actual host os" do 11 | codec = Owasp::Esapi::Codec::OsCodec.new 12 | codec.os.should == Owasp::Esapi::Codec::OsCodec::UNIX_HOST 13 | end 14 | 15 | it "should decode ^< as < for windows" do 16 | win_codec.decode("^<").should == "<" 17 | end 18 | 19 | it "should decode \\< as < for unix" do 20 | unix_codec.decode("\\<").should == "<" 21 | end 22 | 23 | it "should encode c:\\jeff with ^ chars for windows" do 24 | win_codec.encode([],"C:\\jeff").should == "C^:^\\jeff" 25 | end 26 | 27 | it "should encode dir & foo with ^ chars for windows" do 28 | win_codec.encode([],"dir & foo").should == "dir^ ^&^ foo" 29 | 30 | end 31 | 32 | it "should encode c:\\jeff with \\ chars for unix" do 33 | unix_codec.encode(Owasp::Esapi::Encoder::CHAR_ALPHANUMERIC,"C:\\jeff").should == "C\\:\\\\jeff" 34 | end 35 | 36 | it "should encode dir & foo with \\ chars for unix" do 37 | unix_codec.encode([],"dir & foo").should == "dir\\ \\&\\ foo" 38 | end 39 | 40 | it "should encode /etc/hosts with \\ chars for unix" do 41 | unix_codec.encode(['-'],"/etc/hosts").should == "\\/etc\\/hosts" 42 | end 43 | 44 | it "should encode /etc/hosts; ls -l with \\ chars for unix" do 45 | unix_codec.encode(['-'],"/etc/hosts; ls -l").should == "\\/etc\\/hosts\\;\\ ls\\ -l" 46 | end 47 | 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/validator/integer_rule_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Validator 6 | describe IntegerRule do 7 | 8 | it "should validate 89745 as valid within range of 0 to 1000000" do 9 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,10000000) 10 | rule.valid?("","89745").should be_true 11 | end 12 | 13 | it "should fail to validate -1 for range of 0 to 100" do 14 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,100) 15 | rule.valid?("","-1").should be_false 16 | end 17 | 18 | it "should validate 0x100 as valid within range of 0 to 300" do 19 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,300) 20 | rule.valid("","0x100").should == 256 21 | end 22 | 23 | it "should raise an error when a non string is passed in" do 24 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,300) 25 | lambda{ rule.valid("",100) }.should raise_error(TypeError) 26 | end 27 | 28 | it "should validate 0100 as an octal and with range for 0 to 65" do 29 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,65) 30 | rule.valid("","0100").should == 64 31 | end 32 | 33 | it "should validate a bit string 0b0001 as 1 within range of 0 to 2" do 34 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,2) 35 | rule.valid("","0b0001").should == 1 36 | end 37 | 38 | it "should fail to validate testme as a number within any range" do 39 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,0,2) 40 | rule.valid?("","testme").should be_false 41 | end 42 | 43 | it "should validate -1 within range of -5 t0 5" do 44 | rule = Owasp::Esapi::Validator::IntegerRule.new("test",nil,-5,5) 45 | rule.valid?("","-1").should be_true 46 | end 47 | 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "owasp-esapi-ruby" 8 | gem.summary = %Q{Owasp Enterprise Security APIs for Ruby language} 9 | gem.description = File.read(File.join(File.dirname(__FILE__), 'README')) 10 | gem.email = "thesp0nge@owasp.org" 11 | gem.version = File.read(File.join(File.dirname(__FILE__), 'VERSION')) 12 | gem.homepage = "http://github.com/thesp0nge/owasp-esapi-ruby" 13 | gem.authors = File.read(File.join(File.dirname(__FILE__), 'AUTHORS')) 14 | gem.required_ruby_version = '>= 1.9.2' 15 | gem.add_development_dependency "rspec", ">= 1.2.9" 16 | gem.add_development_dependency "yard", ">= 0" 17 | gem.add_development_dependency "antisamy",">= 0.2.1" 18 | gem.add_dependency "antisamy", ">=0.2.1" 19 | 20 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 21 | end 22 | Jeweler::GemcutterTasks.new 23 | rescue LoadError 24 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 25 | end 26 | 27 | require 'rspec/core/rake_task' 28 | RSpec::Core::RakeTask.new(:spec) do |t| 29 | t.pattern = "./spec/**/*_spec.rb" 30 | # Put spec opts in a file named .rspec in root 31 | end 32 | 33 | # require 'spec/rake/spectask' 34 | # Spec::Rake::SpecTask.new(:spec) do |spec| 35 | # spec.libs << 'lib' << 'spec' 36 | # spec.spec_files = FileList['spec/**/*_spec.rb'] 37 | # end 38 | 39 | # Spec::Rake::SpecTask.new(:rcov) do |spec| 40 | # spec.libs << 'lib' << 'spec' 41 | # spec.pattern = 'spec/**/*_spec.rb' 42 | # spec.rcov = true 43 | # end 44 | 45 | task :spec => :check_dependencies 46 | 47 | task :default => :spec 48 | 49 | begin 50 | require 'yard' 51 | YARD::Rake::YardocTask.new 52 | rescue LoadError 53 | task :yardoc do 54 | abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" 55 | end 56 | end 57 | 58 | namespace :prepare do 59 | desc 'Generate ChangeLog' 60 | task :changelog do 61 | system ('git log --format="%ai %cn %s" > ChangeLog') 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/codec/vbscript_codec.rb: -------------------------------------------------------------------------------- 1 | # Implementation of the Codec interface for 'quote' encoding from VBScript. 2 | module Owasp 3 | module Esapi 4 | module Codec 5 | class VbScriptCodec < BaseCodec 6 | 7 | # Encode a String so that it can be safely used in a specific context. 8 | def encode(immune, input) 9 | encoded_string = '' 10 | encoding = false 11 | inquotes = false 12 | encoded_string.encode!(Encoding::UTF_8) 13 | i = 0 14 | input.encode(Encoding::UTF_8).chars do |c| 15 | if Owasp::Esapi::Encoder::CHAR_ALPHANUMERIC.include?(c) or immune.include?(c) 16 | encoded_string << "&" if encoding and i > 0 17 | encoded_string << "\"" if !inquotes and i > 0 18 | encoded_string << c 19 | inquotes = true 20 | encoding = false 21 | else 22 | encoded_string << "\"" if inquotes and i < input.size 23 | encoded_string << "&" if i > 0 24 | encoded_string << encode_char(immune,c) 25 | inquotes = false 26 | encoding = true 27 | end 28 | i += 1 29 | end 30 | encoded_string 31 | end 32 | # Returns quote-encoded character 33 | def encode_char(immune,input) 34 | return input if immune.include?(input) 35 | hex = hex(input) 36 | return input if hex.nil? 37 | return "chrw(#{input.ord})" 38 | end 39 | 40 | # Returns the decoded version of the character starting at index, or 41 | # nil if no decoding is possible. 42 | # 43 | # Formats all are legal both upper/lower case: 44 | # "x - all special characters 45 | # " + chr(x) + " - not supported 46 | 47 | def decode_char(input) 48 | input.mark(); 49 | first = input.next 50 | if first.nil? 51 | input.reset 52 | return nil; 53 | end 54 | # if this is not an encoded character, return null 55 | if first != "\"" 56 | input.reset 57 | return nil 58 | end 59 | input.next 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/validator/integer_rule.rb: -------------------------------------------------------------------------------- 1 | module Owasp 2 | module Esapi 3 | module Validator 4 | class IntegerRule < BaseRule 5 | attr_accessor :min, :max 6 | 7 | def initialize(type,encoder=nil,min=nil,max=nil) 8 | super(type,encoder) 9 | @min = min 10 | @max = max 11 | @min = Integer::MIN if min.nil? 12 | @max = Integer::MAX if max.nil? 13 | end 14 | 15 | # Validate the input context as an integer 16 | def valid(context,input) 17 | if input.nil? 18 | if @allow_nil 19 | return nil 20 | end 21 | user = "#{context}: Input number required" 22 | log = "Input number required: context=#{context}, input=#{input}" 23 | raise Owasp::Esapi::ValidationException.new(user,log,context) 24 | end 25 | clean = @encoder.canonicalize(input) 26 | if @min > @max 27 | user = "#{context}: Invalid number input: context" 28 | log = "Validation parameter error for number: maxValue ( #{max}) must be greater than minValue ( #{min}) for #{context}" 29 | raise Owasp::Esapi::ValidationException.new(user,log,context) 30 | end 31 | begin 32 | user = "Invalid number input must be between #{min} and #{max}: context=#{context}" 33 | log = "Invalid number input must be between #{min} and #{max}: context=#{context}, input=#{input}" 34 | i = Integer(clean) 35 | if i < @min 36 | raise Owasp::Esapi::ValidationException.new(user,log,context) 37 | end 38 | if i > @max 39 | raise Owasp::Esapi::ValidationException.new(user,log,context) 40 | end 41 | return i 42 | rescue Exception => e 43 | user = "#{context}: Input number required" 44 | log = "Input number required: context=#{context}, input=#{input}" 45 | raise Owasp::Esapi::ValidationException.new(user,log,context) 46 | end 47 | end 48 | 49 | # SThis will call valid and return a 0 if its invalid 50 | def sanitize(context,input) 51 | result = 0 52 | begin 53 | result= valid(context,input) 54 | rescue ValidationException => e 55 | end 56 | result 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/codec/os_codec.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | 3 | # Operating system codec for escape characters for HOST commands 4 | # We look at Unix style (max, linux) and Windows style 5 | module Owasp 6 | module Esapi 7 | module Codec 8 | class OsCodec < BaseCodec 9 | # Window Host flag 10 | WINDOWS_HOST = :Windows 11 | # Unix Host flag 12 | UNIX_HOST = :Unix 13 | 14 | # Setup the code, if no os is passed in the codec 15 | # will guess the OS based on the ruby host_os variable 16 | def initialize(os = nil) 17 | @host = nil 18 | @escape_char = '' 19 | host_os = os 20 | if os.nil? 21 | host_os = case Config::CONFIG['host_os'] 22 | when /mswin|windows/i then WINDOWS_HOST 23 | when /linux/i then UNIX_HOST 24 | when /darwin/i then UNIX_HOST 25 | when /sunos|solaris/i then UNIX_HOST 26 | else UNIX_HOST 27 | end 28 | end 29 | if host_os == WINDOWS_HOST 30 | @host = WINDOWS_HOST 31 | @escape_char = '^' 32 | elsif host_os == UNIX_HOST 33 | @host = UNIX_HOST 34 | @escape_char = '\\' 35 | end 36 | end 37 | 38 | # get the configured OS 39 | def os 40 | @host 41 | end 42 | 43 | # Returns shell encoded character 44 | # ^ - for windows 45 | # \\ - for unix 46 | def encode_char(immune,input) 47 | return input if immune.include?(input) 48 | return input if hex(input).nil? 49 | return "#{@escape_char}#{input}" 50 | end 51 | 52 | # Returns the decoded version of the character starting at index, or 53 | # nil if no decoding is possible. 54 | #

55 | # Formats all are legal both upper/lower case: 56 | # ^x - all special characters when configured for WINDOWS 57 | # \\ - all special characters when configured for UNIX 58 | def decode_char(input) 59 | input.mark 60 | first = input.next 61 | # check first char 62 | if first.nil? 63 | input.reset 64 | return nil 65 | end 66 | # if it isnt escape return nil 67 | if first != @escape_char 68 | input.reset 69 | return nil 70 | end 71 | # get teh escape value 72 | return input.next 73 | end 74 | 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/esapi.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Class loading mechanism, we use this to create new instances of objects based 3 | # on config data. This allows a user to set their own config for instance to use thier 4 | # own implmentation of a given class. ClassLoader based on Rails constantize 5 | # 6 | class ClassLoader 7 | def self.load_class(class_name) 8 | # we are using ruby 1.9.2 as a requirement, so we can use the inheritance 9 | # of const_get to find our object. if mis-spelled it will raise a NameError 10 | names = class_name.split("::") 11 | klass = Object 12 | names.each do |name| 13 | klass = klass.const_get(name) 14 | end 15 | klass.new 16 | end 17 | end 18 | 19 | # Owasp root modules 20 | module Owasp 21 | # Configuration class 22 | class Configuration 23 | attr_accessor :logger, :encoder, :resources 24 | 25 | def initialize 26 | @resources = {} 27 | @patterns = {} 28 | end 29 | # Is intrustion detectione nabled? 30 | def ids? 31 | return true 32 | end 33 | # Get the encoder class anem 34 | def get_encoder_class 35 | end 36 | def resource(resource_key) 37 | return @resources[resource_key] 38 | end 39 | def pattern(name) 40 | @patterns[name] 41 | end 42 | def add_pattern(name,regex) 43 | @patterns[name] = regex 44 | end 45 | 46 | def allowed_extenions 47 | %w[.zip .pdf .doc .docx .ppt .pptx .tar .gz. tgz. rar .xls,.rtf .txt .xml .exe .dll] 48 | end 49 | 50 | def max_file_upload 51 | return 1024 52 | end 53 | 54 | # Return a class that can handle a virus scan 55 | def file_scanner 56 | return nil 57 | end 58 | 59 | end 60 | # Logging class stub 61 | class Logger 62 | def warn(msg) 63 | #puts "WARNING: #{msg}" 64 | end 65 | def info(level,msg) 66 | end 67 | end 68 | # Esapi Root module 69 | module Esapi 70 | 71 | # seutp ESAPI 72 | def self.setup 73 | @config ||= Configuration.new 74 | yield @config if block_given? 75 | process_config(@config) 76 | end 77 | 78 | # Get the security configuration context 79 | def self.security_config 80 | @security ||= Configuration.new 81 | end 82 | # Get the configured logger 83 | def self.logger 84 | @logger ||= Logger.new 85 | end 86 | # Get the configured encoded 87 | def self.encoder 88 | @encoder ||= ClassLoader.load_class("Owasp::Esapi::Encoder") 89 | end 90 | 91 | private 92 | # Process the config data to setup esapi 93 | def self.process_config(conf) 94 | end 95 | 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = The Owasp ESAPI Ruby project 2 | 3 | == Introduction 4 | 5 | The Owasp ESAPI Ruby is a port for outstanding release quality Owasp ESAPI 6 | project to the Ruby programming language. 7 | 8 | Ruby is now a famous programming language due to its Rails framework developed by David Heinemeier Hansson (http://twitter.com/dhh) that simplify the creation of a web application using a convention over configuration approach to simplify programmers' life. 9 | 10 | Despite Rails diffusion, there are a lot of Web framework out there that allow people to write web apps in Ruby (merb, sinatra, vintage) [http://accidentaltechnologist.com/ruby/10-alternative-ruby-web-frameworks/]. Owasp Esapi Ruby wants to bring all Ruby deevelopers a gem full of Secure APIs they can use whatever the framework they choose. 11 | 12 | https://secure.travis-ci.org/thesp0nge/owasp-esapi-ruby.png 13 | 14 | == Why supporting only Ruby 1.9.2 and beyond? 15 | 16 | The OWASP Esapi Ruby gem will require at least version 1.9.2 of Ruby interpreter to make sure to have full advantages of the newer language APIs. 17 | 18 | In particular version 1.9.2 introduces radical changes in the following areas: 19 | 20 | === Regular expression engine 21 | (to be written) 22 | 23 | === UTF-8 support 24 | Unicode support in 1.9.2 is much better and provides better support for character set encoding/decoding 25 | * All strings have an additional chunk of info attached: Encoding 26 | * String#size takes encoding into account – returns the encoded character count 27 | * You can get the raw datasize 28 | * Indexed access is by encoded data – characters, not bytes 29 | * You can change encoding by force but it doesn’t convert the data 30 | 31 | === Dates and Time 32 | From "Programming Ruby 1.9" 33 | 34 | "As of Ruby 1.9.2, the range of dates that can be represented is no longer limited by the under- lying operating system’s time representation (so there’s no year 2038 problem). As a result, the year passed to the methods gm, local, new, mktime, and utc must now include the century—a year of 90 now represents 90 and not 1990." 35 | 36 | == Roadmap 37 | 38 | Please see ChangeLog file. 39 | 40 | == Note on Patches/Pull Requests 41 | 42 | * Fork the project. 43 | * Create documentation with rake yard task 44 | * Make your feature addition or bug fix. 45 | * Add tests for it. This is important so I don't break it in a 46 | future version unintentionally. 47 | * Commit, do not mess with rakefile, version, or history. 48 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 49 | * Send me a pull request. Bonus points for topic branches. 50 | 51 | == Copyright 52 | 53 | Copyright (c) 2011 the OWASP Foundation. See LICENSE for details. 54 | -------------------------------------------------------------------------------- /spec/codec/html_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe HtmlCodec do 7 | let (:codec) { Owasp::Esapi::Codec::HtmlCodec.new } 8 | 9 | it "should not change test" do 10 | codec.encode([],"test").should == "test" 11 | end 12 | 13 | it "should encode < as <" do 14 | codec.encode([],"<").should == "<" 15 | end 16 | 17 | it "should encode 0x100 as Ā" do 18 | s = 0x100.chr(Encoding::UTF_8) 19 | m = codec.encode([],s[0]) 20 | m.should == "Ā" 21 | end 22 | 23 | it "should decode test! as test!" do 24 | codec.decode("test!").should == "test!" 25 | end 26 | 27 | it "should skip &jeff; an invlaid attribute" do 28 | codec.decode("&jeff;").should == "&jeff;" 29 | end 30 | 31 | # dynamic tests for various inputs to decode 32 | { 33 | "&" => "&", 34 | "&X" => "&X", 35 | "&" => "&", 36 | "&X" => "&X", 37 | "<" => "<", 38 | "<X" => " "<", 40 | "<X"=> " "<", 42 | "²" => "\u00B2", 43 | "²X" => "\u00B2X", 44 | "²" => "\u00B2", 45 | "²X" => "\u00B2X", 46 | "³" => "\u00B3", 47 | "³X" => "\u00B3X", 48 | "³" => "\u00B3", 49 | "³X" => "\u00B3X", 50 | "¹" => "\u00B9", 51 | "¹X" => "\u00B9X", 52 | "¹" => "\u00B9", 53 | "¹X" => "\u00B9X", 54 | "⊃" => "\u2283", 55 | "⊃X" => "\u2283X", 56 | "&sup" => "\u2283", 57 | "&supX" => "\u2283X", 58 | "⊇" => "\u2287", 59 | "⊇X" => "\u2287X", 60 | "&supe" => "\u2287", 61 | "&supeX" => "\u2287X", 62 | "π" => "\u03C0", 63 | "πX" => "\u03C0X", 64 | "&pi" => "\u03C0", 65 | "&piX" => "\u03C0X", 66 | "ϖ" => "\u03D6", 67 | "ϖX" => "\u03D6X", 68 | "&piv" => "\u03D6", 69 | "&pivX" => "\u03D6X", 70 | "θ" => "\u03B8", 71 | "θX" => "\u03B8X", 72 | "&theta" => "\u03B8", 73 | "&thetaX" => "\u03B8X", 74 | "ϑ" => "\u03D1", 75 | "ϑX" => "\u03D1X", 76 | "&thetasym" => "\u03D1", 77 | "&thetasymX" => "\u03D1X", 78 | }.each_pair do |k,v| 79 | it "should decode #{k} as #{v}" do 80 | codec.decode(k).should == v 81 | end 82 | end 83 | 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/validator/html_rule.rb: -------------------------------------------------------------------------------- 1 | require 'antisamy' 2 | module Owasp 3 | module Esapi 4 | module Validator 5 | 6 | # A validator performs syntax and possibly semantic validation of a single 7 | # piece of data from an untrusted source. This rule invokes AntiSamy sanitization 8 | class HTMLRule < BaseRule 9 | 10 | # Setup the HTML rule 11 | def initialize(type,encoder = nil,whitelist_pattern = nil) 12 | super(type,encoder) 13 | @string_rule = StringRule.new(type,encoder,whitelist_pattern) 14 | @string_rule.canonicalize = true 15 | begin 16 | @@policy ||= AntiSamy.policy(Esapi.security_config.resource("antisamy")) 17 | rescue Exception => e 18 | puts e 19 | raise ConfigurationException.new("Failed to load antisamy policy",e) 20 | end 21 | end 22 | 23 | # set the max length of input 24 | def max=(length) 25 | @string_rule.max = length 26 | end 27 | 28 | # enable the canonicalization flag 29 | def canonicalize=(v) 30 | @string_rule.canonicalize = v 31 | end 32 | 33 | # Remove any disallowed html form the string 34 | def sanitize(context,input) 35 | safe = '' 36 | begin 37 | safe = antisamy(context,input) 38 | rescue Exception => e 39 | end 40 | safe 41 | end 42 | 43 | # Invoke antisamy on the HTML cleaning out anything that didnt match the rules 44 | def antisamy(context,input) 45 | if input.nil? 46 | if @allow_nil 47 | return nil 48 | end 49 | user = "#{context}: Input number required" 50 | log = "Input number required: context=#{context}, input=#{input}" 51 | raise Owasp::Esapi::ValidationException.new(user,log,context) 52 | end 53 | canonical = @string_rule.valid(context,input) 54 | begin 55 | r = AntiSamy.scan(canonical,@@policy) 56 | unless r.messages.empty? 57 | Esapi.logger.info(:SECURITY_FAILURE,"Cleaned up HTML error #{r.messages}") 58 | end 59 | # AntiSamy will wrap loose content in a

if there is no starting tag 60 | clean = r.clean_html 61 | # Strip out the >p> tags 62 | if clean =~ /^\(.*)\<\/p\>$/ 63 | x = $1 64 | unless input =~ /^\ e 70 | user = "#{context}: Invalid HTML input" 71 | log = "Invalid HTML input: context=#{context} error=#{e.message}" 72 | raise Owasp::Esapi::ValidationException.new(user,log,context,e) 73 | end 74 | end 75 | 76 | # Validate the input context as html 77 | def valid(context,input) 78 | antisamy(context,input) 79 | end 80 | 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/validator/float_rule.rb: -------------------------------------------------------------------------------- 1 | module Owasp 2 | module Esapi 3 | module Validator 4 | class FloatRule < BaseRule 5 | attr_accessor :min, :max 6 | 7 | def initialize(type,encoder=nil,min=nil,max=nil) 8 | super(type,encoder) 9 | @min = min 10 | @max = max 11 | @min = Float::MIN if min.nil? 12 | @max = Float::MAX if max.nil? 13 | end 14 | 15 | # Validate the input context as a float 16 | def valid(context,input) 17 | if input.nil? 18 | if @allow_nil 19 | return nil 20 | end 21 | puts "::#{input}::" 22 | user = "#{context}: Input number required" 23 | log = "Input number required: context=#{context}, input=#{input}" 24 | raise Owasp::Esapi::ValidationException.new(user,log,context) 25 | end 26 | clean = @encoder.canonicalize(input) 27 | if @min > @max 28 | user = "#{context}: Invalid number input: context" 29 | log = "Validation parameter error for number: maxValue ( #{max}) must be greater than minValue ( #{min}) for #{context}" 30 | raise Owasp::Esapi::ValidationException.new(user,log,context) 31 | end 32 | begin 33 | user = "Invalid number input must be between #{min} and #{max}: context=#{context}" 34 | log = "Invalid number input must be between #{min} and #{max}: context=#{context}, input=#{input}" 35 | i = Float(clean) 36 | #check min 37 | if i < @min 38 | raise Owasp::Esapi::ValidationException.new(user,log,context) 39 | end 40 | # check max 41 | if i > @max 42 | raise Owasp::Esapi::ValidationException.new(user,log,context) 43 | end 44 | # check infinity 45 | if i.infinite? 46 | user = "#{context}: Invalid number input: context" 47 | log = "Invalid double input is infinite context=#{context} input=#{input}" 48 | raise Owasp::Esapi::ValidationException.new(user,log,context) 49 | end 50 | # checknan 51 | if i.nan? 52 | user = "#{context}: Invalid number input: context" 53 | log = "Invalid double input not a number context=#{context} input=#{input}" 54 | raise Owasp::Esapi::ValidationException.new(user,log,context) 55 | end 56 | return i 57 | rescue Exception => e 58 | user = "#{context}: Input number required" 59 | log = "Input number required: context=#{context}, input=#{input}" 60 | raise Owasp::Esapi::ValidationException.new(user,log,context) 61 | end 62 | end 63 | 64 | # This will call valid and return a 0 if its invalid 65 | def sanitize(context,input) 66 | result = 0 67 | begin 68 | result= valid(context,input) 69 | rescue ValidationException => e 70 | end 71 | result 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/validator/base_rule.rb: -------------------------------------------------------------------------------- 1 | # Expand Integer to add Min and Max values 2 | class Integer #:nodoc: 3 | N_BYTES = [42].pack('i').size 4 | N_BITS = N_BYTES * 8 5 | MAX = 2 ** (N_BITS - 2) - 1 6 | MIN = -MAX - 1 7 | end 8 | 9 | module Owasp 10 | module Esapi 11 | module Validator 12 | 13 | # A ValidationRule performs syntax and possibly semantic validation of a single 14 | # piece of data from an untrusted source. 15 | class BaseRule 16 | attr_accessor :encoder, :name, :allow_nil 17 | def initialize(name,encoder=nil) 18 | @name = name 19 | @encoder = encoder 20 | @encoder = Owasp::Esapi.encoder if @encoder.nil? 21 | @allow_nil = false 22 | end 23 | 24 | # return true if the input passes validation 25 | def valid?(context,input) 26 | valid = false 27 | begin 28 | valid(context,input) 29 | valid = true 30 | rescue Exception =>e 31 | end 32 | valid 33 | end 34 | 35 | # Parse the input, calling the valid method 36 | # if an exception if thrown it will be added 37 | # to the ValidatorErrorList object. This method allows for multiple rules to be executed 38 | # and collect all the errors that were invoked along the way. 39 | def validate(context,input, errors=nil) 40 | valid = nil 41 | begin 42 | valid = valid(context,input) 43 | rescue ValidationException => e 44 | errors<< e unless errors.nil? 45 | end 46 | input 47 | end 48 | 49 | # Parse the input, raise exceptions if validation fails 50 | # sub classes need to implment this method as the base class will always raise an 51 | # exception 52 | def valid(context,input) 53 | raise Owasp::Esapi::ValidationException.new(input,input,context) 54 | end 55 | 56 | # Try to call get *valid*, then call sanitize, finally return a default value 57 | def safe(context,string) 58 | valid = nil 59 | begin 60 | valid = valid(context,input) 61 | rescue ValidationException => e 62 | return sanitize(context,input) 63 | end 64 | return valid 65 | end 66 | 67 | # The method is similar to getSafe except that it returns a 68 | # harmless object that may or may not have any similarity to the original 69 | # input (in some cases you may not care). In most cases this should be the 70 | # same as the getSafe method only instead of throwing an exception, return 71 | # some default value. Subclasses should implment this method 72 | def sanitize(context,input) 73 | input 74 | end 75 | 76 | # Removes characters that aren't in the whitelist from the input String. 77 | # chars is expected to be string 78 | def whitelist(input,list) 79 | rc = '' 80 | input.chars do |c| 81 | rc << c if list.include?(c) 82 | end 83 | rc 84 | end 85 | 86 | end 87 | 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/codec/xml_codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '../../spec_helper') 2 | 3 | module Owasp 4 | module Esapi 5 | module Codec 6 | describe XmlCodec do 7 | let (:codec) { Owasp::Esapi::Codec::XmlCodec.new } 8 | describe 'XML encoding' do 9 | it "should encode nil as nil" do 10 | codec.encode([],nil).should == nil 11 | end 12 | 13 | it "should encode ' ' as ' '" do 14 | codec.encode(Owasp::Esapi::Encoder::IMMUNE_XML," ").should == " " 15 | end 16 | 17 | it "should encode ", 65 | "%3Cscript>alert%28%22hello"%29%3B%3C%2Fscript%3E"=> "", 66 | }.each_pair do |k,v| 67 | it "should canonicalize #{k} to #{v}" do 68 | begin 69 | encoder.canonicalize(k.dup).should == v 70 | rescue IntrustionException =>e 71 | # if IDSis on we would throw an intrustion exception, other exceptions are real errors 72 | end 73 | end 74 | end 75 | 76 | # Javascript dynamic canonicilzation tests 77 | { 78 | "\\0"=> "\0", 79 | "\\b"=> "\b", 80 | "\\t"=> "\t", 81 | "\\n"=> "\n", 82 | "\\v"=> "\v", 83 | "\\f"=> "\f", 84 | "\\r"=> "\r", 85 | "\\'"=> "\'", 86 | "\\\""=> "\"", 87 | "\\\\"=> "\\", 88 | "\\<"=> "<", 89 | }.each_pair do |k,v| 90 | it "should canonicalize javascript #{k} to #{v}" do 91 | begin 92 | jsencoder.canonicalize(k.dup).should == v 93 | rescue IntrustionException =>e 94 | # if IDSis on we would throw an intrustion exception, other exceptions are real errors 95 | end 96 | end 97 | end 98 | # CSS dynamic canonicalization tests 99 | { 100 | "\\3c"=> "<", 101 | "\\03c"=> "<", 102 | "\\003c"=> "<", 103 | "\\0003c"=> "<", 104 | "\\00003c"=> "<", 105 | "\\3C"=> "<", 106 | "\\03C"=> "<", 107 | "\\003C"=> "<", 108 | "\\0003C"=> "<", 109 | "\\00003C"=> "<", 110 | }.each_pair do |k,v| 111 | it "should canonicalize CSS #{k} to #{v}" do 112 | begin 113 | cssencoder.canonicalize(k.dup).should == v 114 | rescue IntrustionException =>e 115 | # if IDSis on we would throw an intrustion exception, other exceptions are real errors 116 | end 117 | end 118 | end 119 | # Sanitize 120 | it "should sanitize input exceptions" do 121 | # test null value 122 | encoder.canonicalize(nil).should == nil 123 | # test exception paths 124 | encoder.sanitize("%25",true).should == '%' 125 | encoder.sanitize("%25",false).should == '%' 126 | end 127 | 128 | # Dynamic double canonicalization tests 129 | { 130 | "&lt;"=> "<",# double entity 131 | "%255c"=> "\\", # double percent 132 | "%2525"=> "%" , #double percent 133 | "%26lt%3b"=> "<", #double percent 134 | "%253c"=> "<", 135 | "%26lt%3b"=> "<", 136 | "%26"=> "&", 137 | "%%33%63"=> "<", 138 | "%%33c"=> "<", 139 | "%3%63"=> "<", 140 | "&lt;"=> "<", 141 | "&%6ct;"=> "<", 142 | "%3c"=> "<", 143 | "%25 %2526 %26#X3c;script> %3Cscript%25252525253e"=> "% & " => "Test.", 120 | "Jeff" => "Jeff", 121 | "Aspect Security" => "Aspect Security", 122 | "Test. <

load=alert()" => "Test. load=alert()", 123 | "Test.
b
" => "

Test.

b
", 124 | "Test. alert(document.cookie)" => "Test. alert(document.cookie)", 125 | "Test. alert(document.cookie)" => "Test. alert(document.cookie)", 126 | "Test. alert(document.cookie)" => "Test. alert(document.cookie)", 127 | }.each_pair do |html,result| 128 | it "should convert #{html} to #{result}" do 129 | validator.valid_html("test",html,100,false).should == result 130 | end 131 | end 132 | end 133 | 134 | describe "-Directory Path Tests-" do 135 | { 136 | "c:\\ridiculus" => ["C:\\",false,Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST], 137 | "c:\\jeff" => ["C:\\",false,Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST], 138 | "c:\\temp\\..\\etc" => ["C:\\",false,Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST], 139 | "C:\\" => ["C:\\",true,Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST], 140 | "C:\\windows" => ["C:\\",true,Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST], 141 | "C:\\windows\\System32" => ["C:\\",true,Owasp::Esapi::Codec::OsCodec::WINDOWS_HOST], 142 | "/tmp" => ["/",true,Owasp::Esapi::Codec::OsCodec::UNIX_HOST], 143 | "/bin/sh" => ["/",true,Owasp::Esapi::Codec::OsCodec::UNIX_HOST], 144 | "/etc" => ["/",true,Owasp::Esapi::Codec::OsCodec::UNIX_HOST], 145 | "/etc/ridiculous" => ["/",false,Owasp::Esapi::Codec::OsCodec::UNIX_HOST], 146 | "/tmp/../etc" => ["/",false,Owasp::Esapi::Codec::OsCodec::UNIX_HOST], 147 | "/bin/sh" => ["/",false,Owasp::Esapi::Codec::OsCodec::UNIX_HOST], 148 | }.each_pair do |path,parent_and_os| 149 | os = Owasp::Esapi::Codec::OsCodec.new.os 150 | it "should check #{path}, and #{parent_and_os[1] ? 'pass' : 'fail'} on #{parent_and_os.last} executing test on #{os}" do 151 | x = validator.valid_directory?("test", path,parent_and_os.first,false) 152 | if os == parent_and_os.last 153 | x.should == parent_and_os[1] 154 | else 155 | x.should be_false 156 | end 157 | end 158 | end 159 | end 160 | 161 | describe "-DateTests-" do 162 | 163 | it "should return true for September 11, 2001 with the default format" do 164 | validator.valid_date?("test", "September 11, 2001",nil,true).should be_true 165 | end 166 | 167 | it "should return false for September 11, 2001 with the '%d-%m-%Y' as the format" do 168 | validator.valid_date?("test", "September 11, 2001",'%d-%m-%Y',true).should be_false 169 | end 170 | 171 | it "should return true for nil with the '%d-%m-%Y' as the format" do 172 | validator.valid_date?("test", nil,'%d-%m-%Y',true).should be_true 173 | end 174 | 175 | it "should return false for nil with the '%d-%m-%Y' as the format with allow_nil set to false" do 176 | validator.valid_date?("test", nil,'%d-%m-%Y',false).should be_false 177 | end 178 | end 179 | 180 | describe "-StringTests-" do 181 | it "should return true for a nil" do 182 | validator.valid_string?("test",nil,"Email",100,true,true).should be_true 183 | end 184 | # test cases that should pass 185 | { 186 | "jeff.williams@aspectsecurity.com" => "Email", 187 | "123.168.100.234" => "IPAddress", 188 | "192.168.1.234" => "IPAddress", 189 | "http://www.aspectsecurity.com"=> "URL", 190 | "078-05-1120"=> "SSN", 191 | "078 05 1120"=> "SSN", 192 | "078051120"=> "SSN", 193 | "c:\\ridiculous" => "DirectoryName", 194 | "c:\\temp\\..\\etc" => "DirectoryName", 195 | "/bin/sh" => 'DirectoryName', 196 | "1234987600000008" => "CreditCard", 197 | }.each_pair do |input,rule| 198 | it "should test #{rule} against #{input} as valid" do 199 | validator.valid_string?("test",input,rule,100,true,true).should be_true 200 | end 201 | end 202 | # test cases that should fail 203 | { 204 | "jeff.williams@@aspectsecurity.com" => "Email", 205 | "jeff.williams@aspectsecurity" => "Email", 206 | "..168.1.234"=> "IPAddress", 207 | "10.x.1.234"=> "IPAddress", 208 | "http:///www.aspectsecurity.com" => "URL", 209 | "http://www.aspect security.com"=> "URL", 210 | "987-65-4320"=> "SSN", 211 | "000-00-0000"=> "SSN", 212 | "(555) 555-5555"=> "SSN", 213 | "test"=> "SSN", 214 | "4417 1234 5678 911Z" => "CreditCard", 215 | "C:test" => "DirectoryPath", 216 | }.each_pair do |input,rule| 217 | it "should test #{rule} against #{input} as invalid" do 218 | validator.valid_string?("test",input,rule,100,false).should be_false 219 | end 220 | end 221 | end 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /lib/codec/html_codec.rb: -------------------------------------------------------------------------------- 1 | # Implementation of the Codec interface for HTML entity encoding. 2 | module Owasp 3 | module Esapi 4 | module Codec 5 | class HtmlCodec < BaseCodec 6 | def initialize 7 | @longest_key = 0 8 | @lookup_map = {} 9 | ENTITY_MAP.each_key do |k| 10 | if k.size > @longest_key 11 | @longest_key += 1 12 | end 13 | @lookup_map[k.downcase] = k 14 | end 15 | end 16 | 17 | # Encodes a Character for safe use in an HTML entity field. 18 | def encode_char(immune, input) 19 | c = input 20 | return input if immune.include?(input) 21 | # check for alpha numeric 22 | hex = hex(input) 23 | return input if hex.nil? 24 | # check to see if we need to replace an entity 25 | if ( c.ord <= 0x1f and c != '\t' and c != '\n' and c != '\r' ) || ( c.ord >= 0x7f and c.ord <= 0x9f ) 26 | hex = REPLACEMENT_HEX 27 | c = REPLACEMENT_CHAR 28 | end 29 | # find the entity name if its possible 30 | ENTITY_MAP.each_pair do |k,v| 31 | return "&#{k};" if v == c.ord 32 | end 33 | #encode as a hex value 34 | "&#x#{hex};" 35 | end 36 | 37 | # Returns the decoded version of the character starting at index, or 38 | # nil if no decoding is possible. 39 | # Formats all are legal both with and without semi-colon, upper/lower case: 40 | # * &#dddd; 41 | # * &#xhhhh; 42 | # * &name; 43 | def decode_char(input) 44 | # mark the input 45 | input.mark 46 | first = input.next 47 | if first.nil? 48 | input.reset 49 | return nil 50 | end 51 | 52 | # this isnt an encoded char 53 | if first != '&' 54 | input.reset 55 | return nil 56 | end 57 | 58 | # test for numeric encodings 59 | second = input.next 60 | if second.nil? 61 | input.reset 62 | return nil 63 | end 64 | if second == '#' 65 | c = numeric_entity(input) 66 | return c unless c.nil? 67 | elsif second =~ /[a-zA-Z]/ 68 | input.push(second) 69 | c = named_entity(input) 70 | return c unless c.nil? 71 | end 72 | input.reset 73 | return nil 74 | end 75 | 76 | # check to see if the input is a numeric entity 77 | def numeric_entity(input) #:nodoc: 78 | first = input.peek 79 | return nil if first.nil? 80 | if first.downcase.eql?("x") 81 | input.next 82 | return parse_hex(input) 83 | end 84 | return parse_number(input) 85 | end 86 | 87 | # check to see if the input is a named entity 88 | def named_entity(input)#:nodoc: 89 | possible = '' 90 | len = min(input.remainder.size,@longest_key) 91 | if input.peek?("&") 92 | input.next 93 | end 94 | found_key = false 95 | last_possible = '' 96 | for i in 0..len do 97 | possible << input.next if input.next? 98 | # we have to find the longest match 99 | # so we dont find sub values 100 | if @lookup_map[possible.downcase] 101 | last_possible = @lookup_map[possible.downcase] 102 | end 103 | end 104 | # no matches found return 105 | return nil if last_possible.empty? 106 | # reset the input and plow through 107 | input.reset 108 | for i in 0..last_possible.size 109 | input.next 110 | end 111 | possible = ENTITY_MAP[last_possible] 112 | input.next if input.peek?(';') 113 | possible.chr(Encoding::UTF_8) 114 | end 115 | # parse a number int he stream 116 | def parse_number(input)#:nodoc: 117 | result = '' 118 | while input.next? 119 | c = input.peek 120 | if c =~ /\d/ 121 | result << c 122 | input.next 123 | elsif c == ';' 124 | input.next 125 | break; 126 | else 127 | break; 128 | end 129 | end 130 | 131 | begin 132 | i = result.to_i 133 | return i.chr(Encoding::UTF_8) if i >= START_CODE_POINT and i <= END_CODE_POINT 134 | rescue Exception => e 135 | end 136 | nil 137 | end 138 | # parse a hex value in the stream 139 | def parse_hex(input)#:nodoc: 140 | result = '' 141 | while input.next? 142 | c = input.peek 143 | if "0123456789ABCDEFabcdef".include?(c) 144 | result << c 145 | input.next 146 | elsif c == ";" 147 | input.next 148 | break 149 | else 150 | break 151 | end 152 | end 153 | begin 154 | i = result.hex 155 | return i.chr(Encoding::UTF_8) if i >= START_CODE_POINT and i <= END_CODE_POINT 156 | rescue Exception => e 157 | end 158 | nil 159 | end 160 | 161 | # Replacement const hex 162 | REPLACEMENT_HEX = "fffd" 163 | # Replacement const char 164 | REPLACEMENT_CHAR = '\ufffd' 165 | 166 | # Map of entities to numeric codes 167 | ENTITY_MAP = { 168 | 'Aacute' => 193, 169 | 'aacute' => 225, 170 | 'Acirc' => 194, 171 | 'acirc' => 226, 172 | 'acute' => 180, 173 | 'AElig' => 198, 174 | 'aelig' => 230, 175 | 'Agrave' => 192, 176 | 'agrave' => 224, 177 | 'alefsym' => 8501, 178 | 'Alpha' => 913, 179 | 'alpha' => 945, 180 | 'amp' => 38, 181 | 'and' => 8743, 182 | 'ang' => 8736, 183 | 'Aring' => 197, 184 | 'aring' => 229, 185 | 'asymp' => 8776, 186 | 'Atilde' => 195, 187 | 'atilde' => 227, 188 | 'Auml' => 196, 189 | 'auml' => 228, 190 | 'bdquo' => 8222, 191 | 'Beta' => 914, 192 | 'beta' => 946, 193 | 'brvbar' => 166, 194 | 'bull' => 8226, 195 | 'cap' => 8745, 196 | 'Ccedil' => 199, 197 | 'ccedil' => 231, 198 | 'cedil' => 184, 199 | 'cent' => 162, 200 | 'Chi' => 935, 201 | 'chi' => 967, 202 | 'circ' => 710, 203 | 'clubs' => 9827, 204 | 'cong' => 8773, 205 | 'copy' => 169, 206 | 'crarr' => 8629, 207 | 'cup' => 8746, 208 | 'curren' => 164, 209 | 'Dagger' => 8225, 210 | 'dagger' => 8224, 211 | 'dArr' => 8659, 212 | 'darr' => 8595, 213 | 'deg' => 176, 214 | 'Delta' => 916, 215 | 'delta' => 948, 216 | 'diams' => 9830, 217 | 'divide' => 247, 218 | 'Eacute' => 201, 219 | 'eacute' => 233, 220 | 'Ecirc' => 202, 221 | 'ecirc' => 234, 222 | 'Egrave' => 200, 223 | 'egrave' => 232, 224 | 'empty' => 8709, 225 | 'emsp' => 8195, 226 | 'ensp' => 8194, 227 | 'Epsilon' => 917, 228 | 'epsilon' => 949, 229 | 'equiv' => 8801, 230 | 'Eta' => 919, 231 | 'eta' => 951, 232 | 'ETH' => 208, 233 | 'eth' => 240, 234 | 'Euml' => 203, 235 | 'euml' => 235, 236 | 'euro' => 8364, 237 | 'exist' => 8707, 238 | 'fnof' => 402, 239 | 'forall' => 8704, 240 | 'frac12' => 189, 241 | 'frac14' => 188, 242 | 'frac34' => 190, 243 | 'frasl' => 8260, 244 | 'Gamma' => 915, 245 | 'gamma' => 947, 246 | 'ge' => 8805, 247 | 'gt' => 62, 248 | 'hArr' => 8660, 249 | 'harr' => 8596, 250 | 'hearts' => 9829, 251 | 'hellip' => 8230, 252 | 'Iacute' => 205, 253 | 'iacute' => 237, 254 | 'Icirc' => 206, 255 | 'icirc' => 238, 256 | 'iexcl' => 161, 257 | 'Igrave' => 204, 258 | 'igrave' => 236, 259 | 'image' => 8465, 260 | 'infin' => 8734, 261 | 'int' => 8747, 262 | 'Iota' => 921, 263 | 'iota' => 953, 264 | 'iquest' => 191, 265 | 'isin' => 8712, 266 | 'Iuml' => 207, 267 | 'iuml' => 239, 268 | 'Kappa' => 922, 269 | 'kappa' => 954, 270 | 'Lambda' => 923, 271 | 'lambda' => 955, 272 | 'lang' => 9001, 273 | 'laquo' => 171, 274 | 'lArr' => 8656, 275 | 'larr' => 8592, 276 | 'lceil' => 8968, 277 | 'ldquo' => 8220, 278 | 'le' => 8804, 279 | 'lfloor' => 8970, 280 | 'lowast' => 8727, 281 | 'loz' => 9674, 282 | 'lrm' => 8206, 283 | 'lsaquo' => 8249, 284 | 'lsquo' => 8216, 285 | 'lt' => 60, 286 | 'macr' => 175, 287 | 'mdash' => 8212, 288 | 'micro' => 181, 289 | 'middot' => 183, 290 | 'minus' => 8722, 291 | 'Mu' => 924, 292 | 'mu' => 956, 293 | 'nabla' => 8711, 294 | 'nbsp' => 160, 295 | 'ndash' => 8211, 296 | 'ne' => 8800, 297 | 'ni' => 8715, 298 | 'not' => 172, 299 | 'notin' => 8713, 300 | 'nsub' => 8836, 301 | 'Ntilde' => 209, 302 | 'ntilde' => 241, 303 | 'Nu' => 925, 304 | 'nu' => 957, 305 | 'Oacute' => 211, 306 | 'oacute' => 243, 307 | 'Ocirc' => 212, 308 | 'ocirc' => 244, 309 | 'OElig' => 338, 310 | 'oelig' => 339, 311 | 'Ograve' => 210, 312 | 'ograve' => 242, 313 | 'oline' => 8254, 314 | 'Omega' => 937, 315 | 'omega' => 969, 316 | 'Omicron' => 927, 317 | 'omicron' => 959, 318 | 'oplus' => 8853, 319 | 'or' => 8744, 320 | 'ordf' => 170, 321 | 'ordm' => 186, 322 | 'Oslash' => 216, 323 | 'oslash' => 248, 324 | 'Otilde' => 213, 325 | 'otilde' => 245, 326 | 'otimes' => 8855, 327 | 'Ouml' => 214, 328 | 'ouml' => 246, 329 | 'para' => 182, 330 | 'part' => 8706, 331 | 'permil' => 8240, 332 | 'perp' => 8869, 333 | 'Phi' => 934, 334 | 'phi' => 966, 335 | 'Pi' => 928, 336 | 'pi' => 960, 337 | 'piv' => 982, 338 | 'plusmn' => 177, 339 | 'pound' => 163, 340 | 'Prime' => 8243, 341 | 'prime' => 8242, 342 | 'prod' => 8719, 343 | 'prop' => 8733, 344 | 'Psi' => 936, 345 | 'psi' => 968, 346 | 'quot' => 34, 347 | 'radic' => 8730, 348 | 'rang' => 9002, 349 | 'raquo' => 187, 350 | 'rArr' => 8658, 351 | 'rarr' => 8594, 352 | 'rceil' => 8969, 353 | 'rdquo' => 8221, 354 | 'real' => 8476, 355 | 'reg' => 174, 356 | 'rfloor' => 8971, 357 | 'Rho' => 929, 358 | 'rho' => 961, 359 | 'rlm' => 8207, 360 | 'rsaquo' => 8250, 361 | 'rsquo' => 8217, 362 | 'sbquo' => 8218, 363 | 'Scaron' => 352, 364 | 'scaron' => 353, 365 | 'sdot' => 8901, 366 | 'sect' => 167, 367 | 'shy' => 173, 368 | 'Sigma' => 931, 369 | 'sigma' => 963, 370 | 'sigmaf' => 962, 371 | 'sim' => 8764, 372 | 'spades' => 9824, 373 | 'sub' => 8834, 374 | 'sube' => 8838, 375 | 'sum' => 8721, 376 | 'sup' => 8835, 377 | 'sup1' => 185, 378 | 'sup2' => 178, 379 | 'sup3' => 179, 380 | 'supe' => 8839, 381 | 'szlig' => 223, 382 | 'Tau' => 932, 383 | 'tau' => 964, 384 | 'there4' => 8756, 385 | 'Theta' => 920, 386 | 'theta' => 952, 387 | 'thetasym' => 977, 388 | 'thinsp' => 8201, 389 | 'THORN' => 222, 390 | 'thorn' => 254, 391 | 'tilde' => 732, 392 | 'times' => 215, 393 | 'trade' => 8482, 394 | 'Uacute' => 218, 395 | 'uacute' => 250, 396 | 'uArr' => 8657, 397 | 'uarr' => 8593, 398 | 'Ucirc' => 219, 399 | 'ucirc' => 251, 400 | 'Ugrave' => 217, 401 | 'ugrave' => 249, 402 | 'uml' => 168, 403 | 'upsih' => 978, 404 | 'Upsilon' => 933, 405 | 'upsilon' => 965, 406 | 'Uuml' => 220, 407 | 'uuml' => 252, 408 | 'weierp' => 8472, 409 | 'Xi' => 926, 410 | 'xi' => 958, 411 | 'Yacute' => 221, 412 | 'yacute' => 253, 413 | 'yen' => 165, 414 | 'Yuml' => 376, 415 | 'yuml' => 255, 416 | 'Zeta' => 918, 417 | 'zeta' => 950, 418 | 'zwj' => 8205, 419 | 'zwnj' => 8204 420 | } 421 | end 422 | end 423 | end 424 | end 425 | -------------------------------------------------------------------------------- /lib/validator.rb: -------------------------------------------------------------------------------- 1 | require 'validator/validator_error_list' 2 | require 'validator/base_rule' 3 | require 'validator/string_rule' 4 | require 'validator/date_rule' 5 | require 'validator/integer_rule' 6 | require 'validator/float_rule' 7 | require 'validator/html_rule' 8 | 9 | module Owasp 10 | module Esapi 11 | module Validator 12 | # Encoder to use for the validator 13 | @@encoder ||= Owasp::Esapi.encoder 14 | 15 | #Change the active encoder used by the validator 16 | def self.encoder=(e) 17 | raise ArgumentError, "invalid encoder" if e.nil? 18 | raise ArgumentError unless e.is_a?(Owasp::Esapi::Encoder) 19 | @@encoder = e 20 | end 21 | 22 | # Calls validate_input and returns true if no exceptions are thrown. 23 | def self.valid_string?(context,input,type,max_len,allow_nil, canonicalize = true) 24 | begin 25 | valid_string(context,input,type,max_len,allow_nil,true) 26 | return true 27 | rescue Exception => e 28 | return false 29 | end 30 | end 31 | 32 | # Returns canonicalized and validated input as a String. Invalid input will generate a descriptive ValidationException, 33 | # and input that is clearly an attack will generate a descriptive IntrusionException. 34 | # if the error_list is given, exceptions will be added to the list instead of being thrown 35 | def self.valid_string(context,input,type,max_len,allow_nil, canonicalize = true, error_list = nil) 36 | begin 37 | string_rule = Owasp::Esapi::Validator::StringRule.new(type,@@encoder) 38 | p = Owasp::Esapi.security_config.pattern(type) 39 | if p.nil? 40 | string_rule.add_whitelist(type) 41 | else 42 | string_rule.add_whitelist(p) 43 | end 44 | string_rule.allow_nil = allow_nil 45 | string_rule.canonicalize = canonicalize 46 | string_rule.max = max_len 47 | return string_rule.valid(context,input) 48 | rescue ValidationException => e 49 | if error_list.nil? 50 | raise e 51 | else 52 | error_list << e 53 | end 54 | end 55 | return "" 56 | end 57 | 58 | 59 | # Calls valid_date and returns true if no exceptions are thrown. 60 | def self.valid_date?(context,input, format, allow_nil) 61 | begin 62 | valid_date(context,input,format,allow_nil) 63 | return true 64 | rescue Exception => e 65 | return false 66 | end 67 | end 68 | 69 | # Returns a valid date as a Date. Invalid input will generate a descriptive ValidationException, and input that is clearly an attack 70 | # will generate a descriptive IntrusionException. 71 | # if the error_list is given, exceptions will be added to the list instead of being thrown 72 | def self.valid_date(context, input, format, allow_nil, error_list = nil) 73 | begin 74 | date_rule = DateRule.new("SimpleDate",@@encoder,format) 75 | date_rule.allow_nil = allow_nil 76 | date_rule.valid(context,input) 77 | rescue ValidationException => e 78 | if error_list.nil? 79 | raise e 80 | else 81 | error_list << e 82 | end 83 | end 84 | return nil 85 | end 86 | 87 | # Calls valid_html and returns true if no exceptions are thrown. 88 | def self.valid_html?(context, input, max_len, allow_nil) 89 | begin 90 | valid_html(context,input,max_len,allow_nil) 91 | return true 92 | rescue Exception => e 93 | return false 94 | end 95 | end 96 | 97 | # Returns canonicalized and validated "safe" HTML that does not contain unwanted scripts in the body, attributes, CSS, URLs, or anywhere else. 98 | # Implementors should reference the OWASP AntiSamy project for ideas 99 | # on how to do HTML validation in a whitelist way, as this is an extremely difficult problem. 100 | # if the error_list is given, exceptions will be added to the list instead of being thrown 101 | def self.valid_html(context,input,max_len,allow_nil,error_list = nil) 102 | begin 103 | html_rule = HTMLRule.new("SafeHTML",@@encoder) 104 | html_rule.allow_nil = allow_nil 105 | html_rule.max = max_len 106 | html_rule.canonicalize = false 107 | return html_rule.valid(context,input) 108 | rescue ValidationException => e 109 | if error_list.nil? 110 | raise e 111 | else 112 | error_list << e 113 | end 114 | end 115 | return "" 116 | end 117 | 118 | # Calls valid_directory and returns true if no exceptions are thrown. 119 | def self.valid_directory?(context, input, parent, allow_nil) 120 | begin 121 | valid_directory(context,input,parent,allow_nil) 122 | return true 123 | rescue Exception => e 124 | return false 125 | end 126 | end 127 | 128 | # Returns a canonicalized and validated directory path as a String, provided that the input 129 | # maps to an existing directory that is an existing subdirectory (at any level) of the specified parent. Invalid input 130 | # will generate a descriptive ValidationException, and input that is clearly an attack 131 | # will generate a descriptive IntrusionException. 132 | # if the error_list is given, exceptions will be added to the list instead of being thrown 133 | def self.valid_directory(context, input, parent, allow_nil, error_list = nil) 134 | begin 135 | # Check for nil 136 | if input.nil? 137 | if allow_nil 138 | return nil 139 | end 140 | user = "#{context}: Input directory path required" 141 | log = "Input directory path required: context=#{context}, input=#{input}" 142 | raise ValidationException.new(user,log,context) 143 | end 144 | cparent = File.expand_path(parent) 145 | cdir = File.expand_path(input) 146 | # if the parent a file object? 147 | raise ValidationException.new("#{context}: Input directory name","Invalid directory, does not exist: context=#{context}, input=#{input}",context) unless File.exists?(cdir) 148 | raise ValidationException.new("#{context}: Input directory name","Invalid directory, not a directory: context=#{context}, input=#{input}",context) unless Dir.exists?(cdir) 149 | raise ValidationException.new("#{context}: Input directory name","Invalid directory, parent does not exist: context=#{context}, input=#{input}",context) unless File.exists?(cparent) 150 | raise ValidationException.new("#{context}: Input directory name","Invalid directory, parent not a directory: context=#{context}, input=#{input}",context) unless Dir.exists?(cparent) 151 | raise ValidationException.new("#{context}: Input directory name","Invalid directory, not inside specified parent: context=#{context}, input=#{input}",context) unless cdir.index(cparent) == 0 152 | clean_path = valid_string(context,cdir,"DirectoryName",255,false) 153 | raise ValidationException.new("#{context}: Input directory name","Invalid directory name does not match the canonical path: context=#{context}, input=#{input}",context) unless clean_path.eql?(input) 154 | return clean_path 155 | rescue ValidationException => e 156 | if error_list.nil? 157 | raise e 158 | else 159 | error_list << e 160 | end 161 | end 162 | return "" 163 | end 164 | 165 | # Calls valid_file_name and returns true if no exceptions are thrown. 166 | def self.valid_file_name?(context, input, allowed_extensions, allow_nil) 167 | begin 168 | valid_file_name(context,input,allowed_extensions,allow_nil) 169 | return true 170 | rescue Exception => e 171 | return false 172 | end 173 | end 174 | 175 | # Returns a canonicalized and validated file name as a String. Implementors should check for allowed file extensions here, as well as allowed file name characters, as declared in "ESAPI.properties". Invalid input 176 | # will generate a descriptive ValidationException, and input that is clearly an attack 177 | # will generate a descriptive IntrusionException. 178 | # if the error_list is given, exceptions will be added to the list instead of being thrown 179 | def self.valid_file_name(context, input, allowed_extensions, allow_nil, error_list = nil) 180 | # detect path manipulation 181 | begin 182 | # check extenion list 183 | if allowed_extensions.nil? or allowed_extensions.empty? 184 | raise ValidationException.new("Internal Error", "valid_file_name called with an empty or null list of allowed Extensions, therefore no files can be uploaded", context); 185 | end 186 | # Check for nil 187 | if input.nil? 188 | if allow_nil 189 | return nil 190 | end 191 | user = "#{context}: Input file name required" 192 | log = "Input file name required: context=#{context}, input=#{input}" 193 | raise ValidationException.new(user,log,context) 194 | end 195 | filename = File.expand_path(input) 196 | dirname = File.dirname(filename) 197 | base_name = File.basename(filename) 198 | clean_name = valid_string(context,base_name,"FileName",255,false) 199 | raise ValidationException.new( "#{context} : Invalid file name", "Invalid directory name does not match the canonical path: context=#{context}, input=#{input}",context) unless filename.index(dirname) 200 | # check extensions 201 | allowed_extensions.each do |ext| 202 | if File.extname(clean_name).include?(ext) 203 | return clean_name 204 | end 205 | end 206 | raise ValidationException.new( "context : Invalid file name does not have valid extension ( #{allowed_extensions})", "Invalid file name does not have valid extension ( #{allowed_extensions} ): context=#{context}, input=#{input}", context ) 207 | rescue ValidationException => e 208 | if error_list.nil? 209 | raise e 210 | else 211 | error_list << e 212 | end 213 | end 214 | end 215 | 216 | # Calls valid_integer and returns true if no exceptions are thrown. 217 | def self.valid_integer?(context, input, min, max, allow_nil) 218 | begin 219 | valid_integer(context,input,min,max,allow_nil) 220 | return true 221 | rescue Exception => e 222 | return false 223 | end 224 | end 225 | 226 | # Returns a validated integer. Invalid input 227 | # will generate a descriptive ValidationException, and input that is clearly an attack 228 | # will generate a descriptive IntrusionException. 229 | # if the error_list is given, exceptions will be added to the list instead of being thrown 230 | def self.valid_integer(context, input, min, max, allow_nil, error_list = nil) 231 | begin 232 | rule = IntegerRule.new("number",@@encoder,min,max) 233 | rule.allow_nil = allow_nil 234 | return rule.valid(context,input) 235 | rescue ValidationException => e 236 | if error_list.nil? 237 | raise e 238 | else 239 | error_list << e 240 | end 241 | end 242 | return 0 243 | end 244 | 245 | # Calls valid_float and returns true if no exceptions are thrown. 246 | def self.valid_float?(context, input, min, max, allow_nil) 247 | begin 248 | valid_float(context,input,min,max,allow_nil) 249 | return true 250 | rescue Exception => e 251 | return false 252 | end 253 | end 254 | 255 | # Returns a validated ifloat. Invalid input 256 | # will generate a descriptive ValidationException, and input that is clearly an attack 257 | # will generate a descriptive IntrusionException. 258 | # if the error_list is given, exceptions will be added to the list instead of being thrown 259 | def self.valid_float(context, input, min, max, allow_nil, error_list = nil) 260 | begin 261 | rule = FloatRule.new("number",@@encoder,min,max) 262 | rule.allow_nil = allow_nil 263 | return rule.valid(context,input) 264 | rescue ValidationException => e 265 | if error_list.nil? 266 | raise e 267 | else 268 | error_list << e 269 | end 270 | end 271 | return 0 272 | end 273 | 274 | # call valid_file_contents and returns true if no exceptions are thrown. 275 | def self.valid_file_contents?(context, input_io, max_bytes, allow_nil) 276 | begin 277 | valid_file_content(context, input_io, max_bytes, allow_nil) 278 | return true 279 | rescue Exception => e 280 | return false 281 | end 282 | end 283 | 284 | # Returns validated file content as a byte array. This is a good place to check for max file size, allowed character sets, and do virus scans. Invalid input 285 | # will generate a descriptive ValidationException, and input that is clearly an attack 286 | # will generate a descriptive IntrusionException. 287 | # if the error_list is given, exceptions will be added to the list instead of being thrown 288 | def self.valid_file_content(context, input_io, max_bytes, allow_nil, error_list = nil) 289 | begin 290 | if input_io.nil? 291 | if allow_nil 292 | return nil 293 | end 294 | user = "#{context}: Input required" 295 | log = "Input required: context=#{context}, input=#{input_io}" 296 | raise ValidationException.new(user,log,context) 297 | end 298 | max_allowed = Esapi.security_config.max_file_upload 299 | input_bytes = 0 300 | input_io.bytes.each do |b| 301 | input_bytes += 1 302 | end 303 | input_io.rewind 304 | 305 | raise ValidationException.new("#{context}: Invalid file content can not exceed #{max_allowed} bytes", "Exceeded ESAPI max length", context ) if input_bytes > max_allowed 306 | raise ValidationException.new( "#{context}: Invalid file content can not exceed #{max_bytes} bytes", "Exceeded maxBytes ( #{input_bytes} )", context ) if input_bytes > max_bytes 307 | if Esapi.security_config.file_scanner 308 | begin 309 | return Esapi.security_config.file_scanner.scan(input_io) 310 | rescue Exception => e 311 | raise ValidationException.new("#{context}: Invalid file content invalid", "Virus scanner failed on content", context) 312 | end 313 | end 314 | input_io.rewind 315 | return input_io 316 | rescue ValidationException => e 317 | if error_list.nil? 318 | raise e 319 | else 320 | error_list << e 321 | end 322 | end 323 | return [] 324 | end 325 | end 326 | end 327 | end 328 | -------------------------------------------------------------------------------- /lib/codec/encoder.rb: -------------------------------------------------------------------------------- 1 | # The Encoder interface contains a number of methods for decoding input and encoding output 2 | # so that it will be safe for a variety of interpreters. To prevent 3 | # double-encoding, callers should make sure input does not already contain encoded characters 4 | # by calling canonicalize. Validator implementations should call canonicalize on user input 5 | # before validating to prevent encoded attacks. 6 | # All of the methods must use a "whitelist" or "positive" security model. 7 | # For the encoding methods, this means that all characters should be encoded, except for a specific list of 8 | # "immune" characters that are known to be safe. 9 | # The Encoder performs two key functions, encoding and decoding. These functions rely 10 | # on a set of codecs that can be found in the org.owasp.esapi.codecs package. These include: 11 | # * CSS Escaping< 12 | # * HTMLEntity Encoding 13 | # * JavaScript Escaping 14 | # * MySQL Escaping 15 | # * Oracle Escaping 16 | # * Percent Encoding (aka URL Encoding) 17 | # * Unix Escaping 18 | # * VBScript Escaping 19 | # * Windows Encoding 20 | 21 | require 'cgi' 22 | require 'base64' 23 | require 'codec/base_codec' 24 | require 'codec/pushable_string' 25 | require 'codec/base_codec' 26 | require 'codec/css_codec' 27 | require 'codec/html_codec' 28 | require 'codec/percent_codec' 29 | require 'codec/javascript_codec' 30 | require 'codec/os_codec' 31 | require 'codec/vbscript_codec' 32 | require 'codec/oracle_codec' 33 | require 'codec/mysql_codec' 34 | require 'codec/xml_codec' 35 | 36 | module Owasp 37 | module Esapi 38 | class Encoder 39 | # 40 | # == Immune Character feilds 41 | # 42 | IMMUNE_CSS = [ ] 43 | IMMUNE_HTMLATTR = [ ',', '.', '-', '_' ] 44 | IMMUNE_HTML = [ ',', '.', '-', '_', ' ' ] 45 | IMMUNE_JAVASCRIPT = [ ',', '.', '_' ] 46 | IMMUNE_VBSCRIPT = [ ',', '.', '_' ] 47 | IMMUNE_XML = [ ',', '.', '-', '_', ' ' ] 48 | IMMUNE_SQL = [ ' ' ] 49 | IMMUNE_OS = [ '-' ] 50 | IMMUNE_XMLATTR = [ ',', '.', '-', '_' ] 51 | IMMUNE_XPATH = [ ',', '.', '-', '_', ' ' ] 52 | PASSWORD_SPECIALS = "!$*-.=?@_" 53 | # == Standard Characetr Sets 54 | CHAR_LCASE = "abcdefghijklmnopqrstuvwxyz" 55 | CHAR_UCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 56 | CHAR_DIGITS = "0123456789" 57 | CHAR_SPECIALS = "!$*+-.=?@^_|~" 58 | CHAR_LETTERS = "#{CHAR_LCASE}#{CHAR_UCASE}" 59 | CHAR_ALPHANUMERIC = "#{CHAR_LETTERS}#{CHAR_DIGITS}" 60 | 61 | # Create the encoder, optionally pass in a list of codecs to use 62 | def initialize(configured_codecs = nil) 63 | # codec list 64 | @codecs = [] 65 | # default codecs 66 | @html_codec = Owasp::Esapi::Codec::HtmlCodec.new 67 | @percent_codec = Owasp::Esapi::Codec::PercentCodec.new 68 | @js_codec = Owasp::Esapi::Codec::JavascriptCodec.new 69 | @vb_codec = Owasp::Esapi::Codec::VbScriptCodec.new 70 | @css_codec = Owasp::Esapi::Codec::CssCodec.new 71 | @xml_codec = Owasp::Esapi::Codec::XmlCodec.new 72 | unless configured_codecs.nil? 73 | configured_codecs.each do |c| 74 | @codecs << c 75 | end 76 | else 77 | # setup some defaults codecs 78 | @codecs << @html_codec 79 | @codecs << @percent_codec 80 | @codecs << @js_codec 81 | end 82 | end 83 | 84 | # This method is equivalent to calling sanitize(input, true) 85 | def canonicalize(input) 86 | # if the input is nil, just return nil 87 | return nil if input.nil? 88 | 89 | # check teh ESAPI config and figure out if we want strict encoding 90 | sanitize(input,Owasp::Esapi.security_config.ids?) 91 | end 92 | 93 | # Sanitization is simply the operation of reducing a possibly encoded 94 | # string down to its simplest form. This is important, because attackers 95 | # frequently use encoding to change their input in a way that will bypass 96 | # validation filters, but still be interpreted properly by the target of 97 | # the attack. Note that data encoded more than once is not something that a 98 | # normal user would generate and should be regarded as an attack. 99 | # Everyone says[http://cwe.mitre.org/data/definitions/180.html] you shouldn't do validation 100 | # without canonicalizing the data first. This is easier said than done. The canonicalize method can 101 | # be used to simplify just about any input down to its most basic form. Note that sanitization doesn't 102 | # handle Unicode issues, it focuses on higher level encoding and escaping schemes. In addition to simple 103 | # decoding, sanitize also handles: 104 | # * Perverse but legal variants of escaping schemes 105 | # * Multiple escaping (%2526 or &lt;) 106 | # * Mixed escaping (%26lt;) 107 | # * Nested escaping (%%316 or &%6ct;) 108 | # * All combinations of multiple, mixed, and nested encoding/escaping (%253c or ┦gt;) 109 | # 110 | # Although ESAPI is able to canonicalize multiple, mixed, or nested encoding, it's safer to not accept 111 | # this stuff in the first place. In ESAPI, the default is "strict" mode that throws an IntrusionException 112 | # if it receives anything not single-encoded with a single scheme. Even if you disable "strict" mode, 113 | # you'll still get warning messages in the log about each multiple encoding and mixed encoding received. 114 | # 115 | def sanitize(input, strict) 116 | # check input again, as someone may just wana call sanitize 117 | return nil if input.nil? 118 | working = input 119 | found_codec = nil 120 | mixed_count = 1 121 | found_count = 0 122 | clean = false 123 | while !clean 124 | clean = true 125 | @codecs.each do |codec| 126 | old = working 127 | working = codec.decode(working) 128 | if !old.eql?(working) 129 | if !found_codec.nil? and found_codec != codec 130 | mixed_count += 1 131 | end 132 | found_codec = codec 133 | if clean 134 | found_count += 1 135 | end 136 | clean = false 137 | end 138 | end 139 | end 140 | # test for strict encoding, and indicate mixed and multiple errors 141 | if found_count >= 2 and mixed_count > 1 142 | if strict 143 | raise Owasp::Esapi::IntrustionException.new("Input validation failure", "Multiple (#{found_count}x) and mixed encoding (#{mixed_count}x) detected in #{input}") 144 | else 145 | Owasp::Esapi.logger.warn("Multiple (#{found_count}x) and mixed encoding (#{mixed_count}x) detected in #{input}") 146 | end 147 | elsif found_count >= 2 148 | if strict 149 | raise Owasp::Esapi::IntrustionException.new("Input validation failure", "Multiple (#{found_count}x) detected in #{input}") 150 | else 151 | Owasp::Esapi.logger.warn("Multiple (#{found_count}x) detected in #{input}") 152 | end 153 | elsif mixed_count > 1 154 | if strict 155 | raise Owasp::Esapi::IntrustionException.new("Input validation failure", "Mixed encoding (#{mixed_count}x) detected in #{input}") 156 | else 157 | Owasp::Esapi.logger.warn("Mixed encoding (#{mixed_count}x) detected in #{input}") 158 | end 159 | end 160 | working 161 | end 162 | 163 | # Encode for Base64. using the url safe input set 164 | def encode_for_base64(input) 165 | return nil if input.nil? 166 | Base64.urlsafe_encode64(input) 167 | end 168 | 169 | # Decode data encoded with BASE-64 encoding. 170 | # it assumes url safe encoding sets 171 | def decode_for_base64(input) 172 | return nil if input.nil? 173 | Base64.urlsafe_decode64(input) 174 | end 175 | 176 | def encode_for_ldap(input) 177 | end 178 | def encode_for_dn(input) 179 | end 180 | 181 | # Encode for use in a URL. This method performs URL encoding[http://en.wikipedia.org/wiki/Percent-encoding] 182 | # on the entire string. 183 | def encode_for_url(input) 184 | return nil if input.nil? 185 | CGI::escape(input) 186 | end 187 | 188 | # Decode from URL. First canonicalize and detect any double-encoding. 189 | # If this check passes, then the data is decoded using URL decoding. 190 | def decode_for_url(input) 191 | return nil if input.nil? 192 | clean = sanitize(input) 193 | CGI::unescape(input,Owasp::Esapi.security_config.encoding) 194 | end 195 | 196 | # Encode data for use in Cascading Style Sheets (CSS) content. 197 | # CSS Syntax[http://www.w3.org/TR/CSS21/syndata.html#escaped-characters] (w3.org) 198 | def encode_for_css(input) 199 | return nil if input.nil? 200 | @css_codec.encode(IMMUNE_CSS,input) 201 | end 202 | 203 | # Encode data for insertion inside a data value or function argument in JavaScript. Including user data 204 | # directly inside a script is quite dangerous. Great care must be taken to prevent including user data 205 | # directly into script code itself, as no amount of encoding will prevent attacks there. 206 | # 207 | # Please note there are some JavaScript functions that can never safely receive untrusted data 208 | # as input – even if the user input is encoded. 209 | # 210 | # For example: 211 | # 212 | # 215 | # 216 | def encode_for_javascript(input) 217 | return nil if input.nil? 218 | @js_codec.encode(IMMUNE_JAVASCRIPT,input) 219 | end 220 | 221 | # Encode data for use in HTML using HTML entity encoding 222 | #

223 | # Note that the following characters: 224 | # 00-08, 0B-0C, 0E-1F, and 7F-9F 225 | # cannot be used in HTML. 226 | # 227 | # * HTML Encodings[http://en.wikipedia.org/wiki/Character_encodings_in_HTML] (wikipedia.org) 228 | # * SGML Specification[http://www.w3.org/TR/html4/sgml/sgmldecl.html] (w3.org) 229 | # * XML Specification[http://www.w3.org/TR/REC-xml/#charsets] (w3.org) 230 | def encode_for_html(input) 231 | return nil if input.nil? 232 | @html_codec.encode(IMMUNE_HTML,input) 233 | end 234 | 235 | # Decodes HTML entities. 236 | def dencode_for_html(input) 237 | return nil if input.nil? 238 | @html_codec.decode(input) 239 | end 240 | 241 | # Encode data for use in HTML attributes. 242 | def encode_for_html_attr(input) 243 | return nil if input.nil? 244 | @html_codec.encode(IMMUNE_HTMLATTR,input) 245 | end 246 | 247 | # Encode for an operating system command shell according to the configured OS codec 248 | # 249 | # Please note the following recommendations before choosing to use this method: 250 | # 251 | # 1. It is strongly recommended that applications avoid making direct OS system calls if possible as such calls are not portable, and they are potentially unsafe. Please use language provided features if at all possible, rather than native OS calls to implement the desired feature. 252 | # 2. If an OS call cannot be avoided, then it is recommended that the program to be invoked be invoked directly (e.g., Kernel.system("nameofcommand","parameterstocommand")) as this avoids the use of the command shell. The "parameterstocommand" should of course be validated before passing them to the OS command. 253 | # 3. If you must use this method, then we recommend validating all user supplied input passed to the command shell as well, in addition to using this method in order to make the command shell invocation safe. 254 | # 255 | # An example use of this method would be: Kernel.system("dir" ,encode_for_os(WindowsCodec, "parameter(s)tocommandwithuserinput"); 256 | def encode_for_os(codec,input) 257 | return nil if input.nil? 258 | codec.encode(IMMUNE_OS,input) 259 | end 260 | 261 | # Encode data for insertion inside a data value in a Visual Basic script. Putting user data directly 262 | # inside a script is quite dangerous. Great care must be taken to prevent putting user data 263 | # directly into script code itself, as no amount of encoding will prevent attacks there. 264 | # 265 | # This method is not recommended as VBScript is only supported by Internet Explorer 266 | def encode_for_vbscript(input) 267 | return nil if input.nil? 268 | @vb_codec.encode(IMMUNE_VBSCRIPT,input) 269 | end 270 | 271 | # Encode input for use in a SQL query, according to the selected codec 272 | # (appropriate codecs include the MySQLCodec and OracleCodec). 273 | # 274 | # This method is not recommended. The use of the PreparedStatement 275 | # interface is the preferred approach. However, if for some reason 276 | # this is impossible, then this method is provided as a weaker 277 | # alternative. 278 | # 279 | # The best approach is to make sure any single-quotes are double-quoted. 280 | def encode_for_sql(codec,input) 281 | return nil if input.nil? 282 | codec.encode(IMMUNE_SQL,input) 283 | end 284 | 285 | # Encode data for use in an XPath query. 286 | # 287 | # NB: The reference implementation encodes almost everything and may over-encode. 288 | # 289 | # The difficulty with XPath encoding is that XPath has no built in mechanism for escaping 290 | # characters. It is possible to use XQuery in a parameterized way to 291 | # prevent injection. 292 | # 293 | # For more information, refer to this article[http://www.ibm.com/developerworks/xml/library/x-xpathinjection.html] 294 | # which specifies the following list of characters as the most dangerous: ^&"*';<>(). 295 | # 296 | # This[http://www.packetstormsecurity.org/papers/bypass/Blind_XPath_Injection_20040518.pdf] paper suggests disallowing ' and " in queries.

297 | # * XPath Injection[http://www.ibm.com/developerworks/xml/library/x-xpathinjection.html] (ibm.com) 298 | # * Blind XPath Injection[http://www.packetstormsecurity.org/papers/bypass/Blind_XPath_Injection_20040518.pdf] (packetstormsecurity.org) 299 | def encode_for_xpath(input) 300 | return nil if input.nil? 301 | @xml_codec.encode(IMMUNE_XPATH,input) 302 | end 303 | 304 | # Encode data for use in an XML element. The implementation should follow the 305 | # XML Encoding Standard[http://www.w3schools.com/xml/xml_encoding.asp] from the W3C. 306 | #

307 | # The use of a real XML parser is strongly encouraged. However, in the 308 | # hopefully rare case that you need to make sure that data is safe for 309 | # inclusion in an XML document and cannot use a parse, this method provides 310 | # a safe mechanism to do so. 311 | def encode_for_xml(input) 312 | return nil if input.nil? 313 | @xml_codec.encode(IMMUNE_XML,input) 314 | end 315 | 316 | # Encode data for use in an XML attribute. The implementation should follow 317 | # the XML Encoding Standard[http://www.w3schools.com/xml/xml_encoding.asp] from the W3C. 318 | #

319 | # The use of a real XML parser is highly encouraged. However, in the 320 | # hopefully rare case that you need to make sure that data is safe for 321 | # inclusion in an XML document and cannot use a parse, this method provides 322 | # a safe mechanism to do so. 323 | def encode_for_xml_attr(input) 324 | return nil if input.nil? 325 | @xml_codec.encode(IMMUNE_XMLATTR,input) 326 | end 327 | 328 | end 329 | end 330 | end 331 | --------------------------------------------------------------------------------