owasp-esapi-ruby 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/AUTHORS +5 -0
- data/ChangeLog +69 -0
- data/ISSUES +0 -0
- data/LICENSE +24 -0
- data/README +51 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/lib/codec/base_codec.rb +99 -0
- data/lib/codec/css_codec.rb +101 -0
- data/lib/codec/encoder.rb +330 -0
- data/lib/codec/html_codec.rb +424 -0
- data/lib/codec/javascript_codec.rb +119 -0
- data/lib/codec/mysql_codec.rb +131 -0
- data/lib/codec/oracle_codec.rb +46 -0
- data/lib/codec/os_codec.rb +78 -0
- data/lib/codec/percent_codec.rb +53 -0
- data/lib/codec/pushable_string.rb +114 -0
- data/lib/codec/vbscript_codec.rb +64 -0
- data/lib/codec/xml_codec.rb +173 -0
- data/lib/esapi.rb +68 -0
- data/lib/exceptions.rb +37 -0
- data/lib/executor.rb +20 -0
- data/lib/owasp-esapi-ruby.rb +13 -0
- data/lib/sanitizer/xss.rb +59 -0
- data/lib/validator/base_rule.rb +90 -0
- data/lib/validator/date_rule.rb +92 -0
- data/lib/validator/email.rb +29 -0
- data/lib/validator/float_rule.rb +76 -0
- data/lib/validator/generic_validator.rb +26 -0
- data/lib/validator/integer_rule.rb +61 -0
- data/lib/validator/string_rule.rb +146 -0
- data/lib/validator/validator_error_list.rb +48 -0
- data/lib/validator/zipcode.rb +27 -0
- data/spec/codec/css_codec_spec.rb +61 -0
- data/spec/codec/html_codec_spec.rb +87 -0
- data/spec/codec/javascript_codec_spec.rb +45 -0
- data/spec/codec/mysql_codec_spec.rb +44 -0
- data/spec/codec/oracle_codec_spec.rb +23 -0
- data/spec/codec/os_codec_spec.rb +51 -0
- data/spec/codec/percent_codec_spec.rb +34 -0
- data/spec/codec/vbcript_codec_spec.rb +23 -0
- data/spec/codec/xml_codec_spec.rb +83 -0
- data/spec/owasp_esapi_encoder_spec.rb +226 -0
- data/spec/owasp_esapi_executor_spec.rb +9 -0
- data/spec/owasp_esapi_ruby_email_validator_spec.rb +39 -0
- data/spec/owasp_esapi_ruby_xss_sanitizer_spec.rb +66 -0
- data/spec/owasp_esapi_ruby_zipcode_validator_spec.rb +42 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/validator/base_rule_spec.rb +29 -0
- data/spec/validator/date_rule_spec.rb +40 -0
- data/spec/validator/float_rule_spec.rb +31 -0
- data/spec/validator/integer_rule_spec.rb +51 -0
- data/spec/validator/string_rule_spec.rb +103 -0
- data/spec/validator_skeleton.rb +150 -0
- metadata +235 -0
@@ -0,0 +1,173 @@
|
|
1
|
+
# Implementation of the Codec interface for XML entity encoding.
|
2
|
+
# This differes from HTML entity encoding in that only the following
|
3
|
+
# named entities are predefined:
|
4
|
+
# * lt
|
5
|
+
# * gt
|
6
|
+
# * amp
|
7
|
+
# * apos
|
8
|
+
# * quot
|
9
|
+
#
|
10
|
+
# However, the XML Specification 1.0 states in section 4.6 "Predefined
|
11
|
+
# Entities" that these should still be declared for interoperability
|
12
|
+
# purposes. As such, encoding in this class will not use them.
|
13
|
+
#
|
14
|
+
# It's also worth noting that unlike the HTMLEntityCodec, a trailing
|
15
|
+
# semicolon is required and all valid codepoints are accepted.
|
16
|
+
#
|
17
|
+
# Note that it is a REALLY bad idea to use this for decoding as an XML
|
18
|
+
# document can declare arbitrary entities that this Codec has no way
|
19
|
+
# of knowing about. Decoding is included for completeness but it's use
|
20
|
+
# is not recommended. Use a XML parser instead!
|
21
|
+
|
22
|
+
module Owasp
|
23
|
+
module Esapi
|
24
|
+
module Codec
|
25
|
+
class XmlCodec < BaseCodec
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@longest_key = 0
|
29
|
+
@lookup_map = {}
|
30
|
+
ENTITY_MAP.each_key do |k|
|
31
|
+
if k.size > @longest_key
|
32
|
+
@longest_key += 1
|
33
|
+
end
|
34
|
+
@lookup_map[k.downcase] = k
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Encodes a Character using XML entities as necessary.
|
39
|
+
def encode_char(immune,input)
|
40
|
+
return input if immune.include?(input)
|
41
|
+
return input if input =~ /[a-zA-Z0-9\\t ]/
|
42
|
+
return "&#x#{hex(input)};"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the decoded version of the character starting at index, or
|
46
|
+
# nil if no decoding is possible.
|
47
|
+
def decode_char(input)
|
48
|
+
input.mark
|
49
|
+
result = nil
|
50
|
+
# check first
|
51
|
+
first = input.next
|
52
|
+
return nil if first.nil?
|
53
|
+
return first unless first == "&"
|
54
|
+
# check second
|
55
|
+
second = input.next
|
56
|
+
if second == "#"
|
57
|
+
result = numeric_entity(input)
|
58
|
+
elsif second =~ /[a-zA-Z]/
|
59
|
+
input.push(second)
|
60
|
+
result = named_entity(input)
|
61
|
+
else
|
62
|
+
input.push(second)
|
63
|
+
return nil
|
64
|
+
end
|
65
|
+
|
66
|
+
if result.nil?
|
67
|
+
input.reset
|
68
|
+
end
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
def numeric_entity(input) #:nodoc:
|
73
|
+
first = input.peek
|
74
|
+
return nil if first.nil?
|
75
|
+
if first.downcase.eql?("x")
|
76
|
+
input.next
|
77
|
+
return parse_hex(input)
|
78
|
+
end
|
79
|
+
return parse_number(input)
|
80
|
+
end
|
81
|
+
|
82
|
+
# parse the hex value back to its decimal value
|
83
|
+
def parse_hex(input) #:nodoc:
|
84
|
+
result = ''
|
85
|
+
while input.next?
|
86
|
+
c = input.peek
|
87
|
+
if "0123456789ABCDEFabcdef".include?(c)
|
88
|
+
result << c
|
89
|
+
input.next
|
90
|
+
elsif c == ";"
|
91
|
+
input.next
|
92
|
+
break
|
93
|
+
else
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
begin
|
98
|
+
i = result.hex
|
99
|
+
return i.chr(Encoding::UTF_8) if i >= START_CODE_POINT and i <= END_CODE_POINT
|
100
|
+
rescue Exception => e
|
101
|
+
end
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# parse a number out of the encoded value
|
106
|
+
def parse_number(input) #:nodoc:
|
107
|
+
result = ''
|
108
|
+
missing_semi = true
|
109
|
+
while input.next?
|
110
|
+
c = input.peek
|
111
|
+
if c =~ /\d/
|
112
|
+
result << c
|
113
|
+
input.next
|
114
|
+
elsif c == ';'
|
115
|
+
input.next
|
116
|
+
break;
|
117
|
+
elsif not c =~ /\d/
|
118
|
+
return nil
|
119
|
+
else
|
120
|
+
break;
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
begin
|
125
|
+
i = result.to_i
|
126
|
+
return i.chr(Encoding::UTF_8) if i >= START_CODE_POINT and i <= END_CODE_POINT
|
127
|
+
rescue Exception => e
|
128
|
+
end
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# extract the named entity fromt he input
|
133
|
+
# we convert the entity to the real character i.e. & becoems &
|
134
|
+
def named_entity(input) #:nodoc:
|
135
|
+
possible = ''
|
136
|
+
len = min(input.remainder.size,@longest_key+1)
|
137
|
+
found_key = false
|
138
|
+
last_possible = ''
|
139
|
+
for i in 0..len do
|
140
|
+
possible << input.next if input.next?
|
141
|
+
# we have to find the longest match
|
142
|
+
# so we dont find sub values
|
143
|
+
if @lookup_map[possible.downcase]
|
144
|
+
last_possible = @lookup_map[possible.downcase]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
# no matches found return
|
148
|
+
return nil if last_possible.empty?
|
149
|
+
return nil unless possible.include?(";")
|
150
|
+
# reset the input and plow through
|
151
|
+
input.reset
|
152
|
+
for i in 0..last_possible.size
|
153
|
+
input.next if input.next?
|
154
|
+
end
|
155
|
+
possible = ENTITY_MAP[last_possible]
|
156
|
+
input.next # consume the ;
|
157
|
+
return possible unless possible.empty?
|
158
|
+
return nil
|
159
|
+
end
|
160
|
+
|
161
|
+
# Entity maps
|
162
|
+
ENTITY_MAP = {
|
163
|
+
"lt" => "<",
|
164
|
+
"gt" => ">",
|
165
|
+
"amp" => "&",
|
166
|
+
"apos" => "\'",
|
167
|
+
"quot" => "\""
|
168
|
+
}
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/esapi.rb
ADDED
@@ -0,0 +1,68 @@
|
|
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
|
24
|
+
# Is intrustion detectione nabled?
|
25
|
+
def ids?
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
# Get the encoder class anem
|
29
|
+
def get_encoder_class
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
# Logging class stub
|
34
|
+
class Logger
|
35
|
+
def warn(msg)
|
36
|
+
#puts "WARNING: #{msg}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# Esapi Root module
|
40
|
+
module Esapi
|
41
|
+
|
42
|
+
# seutp ESAPI
|
43
|
+
def self.setup
|
44
|
+
@config ||= Configuration.new
|
45
|
+
yield @config if block_given?
|
46
|
+
process_config(@config)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get the security configuration context
|
50
|
+
def self.security_config
|
51
|
+
@security ||= Configuration.new
|
52
|
+
end
|
53
|
+
# Get the configured logger
|
54
|
+
def self.logger
|
55
|
+
@logger ||= Logger.new
|
56
|
+
end
|
57
|
+
# Get the configured encoded
|
58
|
+
def self.encoder
|
59
|
+
@encoder ||= ClassLoader.load_class("Owasp::Esapi::Encoder")
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
# Process the config data to setup esapi
|
64
|
+
def self.process_config(conf)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
data/lib/exceptions.rb
ADDED
@@ -0,0 +1,37 @@
|
|
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 :log_message
|
8
|
+
def initialize(user_msg, log_msg)
|
9
|
+
super(user_msg)
|
10
|
+
@log_message = log_msg
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Exception throw if there is an error during Executor processing
|
15
|
+
class ExecutorException < EnterpriseSecurityException
|
16
|
+
end
|
17
|
+
|
18
|
+
# Intrustion detection exception to be logged
|
19
|
+
class IntrustionException < Exception
|
20
|
+
attr :log_message
|
21
|
+
def initialize(user_message,log_message)
|
22
|
+
super(user_message)
|
23
|
+
@log_message = log_message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# ValidatorException used in the rule sets
|
28
|
+
class ValidationException < EnterpriseSecurityException
|
29
|
+
attr :context
|
30
|
+
def initialize(user_msg,log_msg,context)
|
31
|
+
super(user_msg,log_msg)
|
32
|
+
@context = context
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/lib/executor.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'esapi'
|
2
|
+
require 'exceptions'
|
3
|
+
require 'sanitizer/xss'
|
4
|
+
require 'validator/zipcode'
|
5
|
+
require 'validator/email'
|
6
|
+
require 'codec/encoder'
|
7
|
+
require 'validator/validator_error_list'
|
8
|
+
require 'validator/base_rule'
|
9
|
+
require 'validator/string_rule'
|
10
|
+
require 'validator/date_rule'
|
11
|
+
require 'validator/integer_rule'
|
12
|
+
require 'validator/float_rule'
|
13
|
+
require 'executor'
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Owasp
|
2
|
+
module Esapi
|
3
|
+
module Sanitizer
|
4
|
+
|
5
|
+
# This is the Cross site scripting sanitizer class.
|
6
|
+
# {http://bit.ly/AJVmn The XSS Cheat sheet at Owasp site}
|
7
|
+
class Xss
|
8
|
+
|
9
|
+
attr_accessor :smart
|
10
|
+
# Creates a new sanitizer
|
11
|
+
# @param [Boolean], smart.
|
12
|
+
# A boolean that says if sanitizer can blindly escape all 'dangerous' characters
|
13
|
+
# in their html entity or rather if it should try to guess if the string needs
|
14
|
+
# sanitizing is a xss attack vector or not and then let the string to pass by.
|
15
|
+
def initialize(smart=false)
|
16
|
+
self.smart= smart
|
17
|
+
end
|
18
|
+
|
19
|
+
# Todo, we should really investigate if dangerous chars have to be trimmed or substituted.
|
20
|
+
# I'm (Paolo) choosing substitute right now... we'll change it later.
|
21
|
+
# @param [String], tainted. The string needs to be sanitized
|
22
|
+
# @return [String] the input string sanitized equivalent
|
23
|
+
def sanitize(tainted)
|
24
|
+
untainted = tainted
|
25
|
+
|
26
|
+
untainted = rule1_sanitize(tainted)
|
27
|
+
|
28
|
+
# Start - RULE #2 - Attribute Escape Before Inserting Untrusted Data into HTML Common Attributes
|
29
|
+
# End - RULE #2 - Attribute Escape Before Inserting Untrusted Data into HTML Common Attributes
|
30
|
+
|
31
|
+
# Start - RULE #3 - JavaScript Escape Before Inserting Untrusted Data into HTML JavaScript Data Values
|
32
|
+
# End - RULE #3 - JavaScript Escape Before Inserting Untrusted Data into HTML JavaScript Data Values
|
33
|
+
|
34
|
+
# Start - RULE #4 - CSS Escape Before Inserting Untrusted Data into HTML Style Property Values
|
35
|
+
# End - RULE #4 - CSS Escape Before Inserting Untrusted Data into HTML Style Property Values
|
36
|
+
|
37
|
+
untainted
|
38
|
+
end
|
39
|
+
private
|
40
|
+
def rule1_sanitize(taint)
|
41
|
+
untainted = taint
|
42
|
+
# Start - RULE #1 - HTML Escape Before Inserting Untrusted Data into HTML Element Content
|
43
|
+
|
44
|
+
# This *must* be the first substitution, otherwise it will substitute also & characters in
|
45
|
+
# valid HTML entities
|
46
|
+
untainted = untainted.gsub("&", "&")
|
47
|
+
untainted = untainted.gsub("<", "<")
|
48
|
+
untainted = untainted.gsub(">", ">")
|
49
|
+
untainted = untainted.gsub("\"", """)
|
50
|
+
untainted = untainted.gsub("\'", "'")
|
51
|
+
untainted = untainted.gsub("/", "/")
|
52
|
+
|
53
|
+
# End - RULE #1 - HTML Escape Before Inserting Untrusted Data into HTML Element Content
|
54
|
+
untainted
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,90 @@
|
|
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 <b>may or may not have any similarity to the original
|
69
|
+
# input (in some cases you may not care)</b>. 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
|