cross 0.30.0 → 0.50.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72b79605b86769c7845420cf772fe7e5ca4251c9
4
+ data.tar.gz: de0ec4524597cd129cd1957bdca99587b743f0fa
5
+ SHA512:
6
+ metadata.gz: 4164f2710605496b67199f45f03f4b680c3926909f949b825f16b6a799534afca8c56496275c1d5f5e9a80139268ca0e90e2d189367981c14c64920732441c29
7
+ data.tar.gz: 8cec8b9fb129209cdf5266b5e046bff9265f06aec82c68dc00c8bb0727a259ca2715bfbfd94790d62356d160ed347efa9a3ee44ff30aeb0f13a2efce9726ec36
data/bin/cross CHANGED
@@ -5,16 +5,25 @@ require 'logger'
5
5
  require 'mechanize'
6
6
  require 'cross'
7
7
  require 'getoptlong'
8
+ require 'codesake-commons'
8
9
 
9
10
  opts = GetoptLong.new(
10
11
  [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
11
12
  [ '--version', '-v', GetoptLong::NO_ARGUMENT ],
12
13
  ['--debug', '-D', GetoptLong::NO_ARGUMENT ],
13
- ['--exploit-url', '-u', GetoptLong::NO_ARGUMENT]
14
+ ['--oneshot', '-1', GetoptLong::NO_ARGUMENT ],
15
+ ['--sample-post', '-S', GetoptLong::REQUIRED_ARGUMENT ],
16
+ ['--tamper', '-t', GetoptLong::REQUIRED_ARGUMENT ],
17
+ ['--exploit-url', '-u', GetoptLong::NO_ARGUMENT ],
18
+ ['--crawl', '-c', GetoptLong::OPTIONAL_ARGUMENT ],
19
+ ['--user', '-U', GetoptLong::REQUIRED_ARGUMENT ],
20
+ ['--password', '-P', GetoptLong::REQUIRED_ARGUMENT ]
14
21
  )
15
22
  trap("INT") { puts '['+'INTERRUPTED'.color(:red)+']'; exit -1 }
16
23
 
17
- options={:exploit_url=>false, :debug=>false}
24
+ options={:exploit_url=>false, :debug=>false, :oneshot=>false, :sample_post=>"", :parameter_to_tamper=>"", :crawl=>{:enabled=>false, :url_prefix=>nil}, :auth=>{:username=>nil, :password=>nil}}
25
+ $logger = Codesake::Commons::Logging.instance
26
+ $logger.toggle_syslog
18
27
 
19
28
  opts.each do |opt, arg|
20
29
  case opt
@@ -26,19 +35,55 @@ opts.each do |opt, arg|
26
35
  puts " -h: this help"
27
36
  exit 0
28
37
  when '--version'
29
- puts "cross " + Cross::VERSION
38
+ puts "cross " + Cross::VERSION + " (C) 2011, 2012 - paolo@armoredcode.com"
30
39
  exit 0
31
- when '--debug'
32
- options[:debug]=true
33
- when '--exploit-url'
34
- options[:exploit_url]=true
40
+ when '--oneshot'
41
+ options[:oneshot] = true
42
+ when '--tamper'
43
+ # This option force cross to tamper only the specified form field
44
+ options[:parameter_to_tamper] = arg unless arg.nil?
45
+ when '--sample-post'
46
+ options[:sample_post] = arg unless File.exist?(arg)
47
+ options[:sample_post] = File.read(arg) if File.exist?(arg) && File.readable?(arg)
48
+ when '--debug'
49
+ options[:debug]=true
50
+ when '--exploit-url'
51
+ options[:exploit_url]=true
52
+
53
+ when '--crawl'
54
+ options[:crawl][:enabled]=true
55
+ options[:crawl][:url_prefix] = arg unless arg.nil?
56
+ when '--user'
57
+ options[:auth][:username]=arg
58
+ when '--password'
59
+ options[:auth][:password]=arg
35
60
  end
36
61
  end
37
62
 
38
- puts "cross " + Cross::VERSION + " (C) 2011, 2012 - paolo@armoredcode.com"
63
+ $logger.helo "cross " + Cross::VERSION + " is starting up"
39
64
 
40
65
  engine = Cross::Engine.instance
41
66
  engine.start(options)
42
67
 
43
- raise "cross: missing target" if ARGV.length != 1
44
- puts "Canary found in output page. Suspected XSS" if engine.inject(ARGV.shift)
68
+ found = false
69
+ $logger.die "missing target" if ARGV.length != 1
70
+ $logger.die "-S and -t flag must be used together" if (options[:sample_post].empty? && ! options[:parameter_to_tamper].empty?) or (! options[:sample_post].empty? && options[:parameter_to_tamper].empty?)
71
+
72
+ if engine.crawl?
73
+ result = engine.crawl(ARGV.shift)
74
+ $logger.die result[:message] if result[:status] == 'KO'
75
+
76
+
77
+ result[:links].each do |l|
78
+ $logger.log "Exploiting: #{options[:crawl][:url_prefix]+l}"
79
+
80
+ found = engine.inject(options[:crawl][:url_prefix]+l)
81
+ $logger.ok "Canary found in output page. Suspected XSS" if found
82
+ end
83
+ else
84
+ found = engine.inject(ARGV.shift)
85
+ $logger.ok "Canary found in output page. Suspected XSS" if found
86
+ end
87
+
88
+ $logger.err "Canary not found" if ! found
89
+ $logger.helo "cross is leaving"
data/cross.gemspec CHANGED
@@ -22,4 +22,6 @@ Gem::Specification.new do |gem|
22
22
  gem.add_dependency "mechanize"
23
23
  gem.add_dependency "logger"
24
24
  gem.add_dependency "rainbow"
25
+
26
+ gem.add_dependency "codesake-commons"
25
27
  end
data/lib/cross.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  require 'cross/version'
2
2
  require 'cross/engine'
3
-
3
+ require 'cross/url'
data/lib/cross/engine.rb CHANGED
@@ -13,6 +13,10 @@ module Cross
13
13
  attr_reader :agent
14
14
  attr_accessor :options
15
15
 
16
+ def debug?
17
+ @options[:debug]
18
+ end
19
+
16
20
  # Starts the engine
17
21
  def start(options={:exploit_url=>false, :debug=>false, :auth=>{}})
18
22
  @agent = Mechanize.new {|a| a.log = Logger.new("cross.log")}
@@ -21,55 +25,126 @@ module Cross
21
25
  @options = options
22
26
  end
23
27
 
24
- def inject(url)
25
- if @agent.nil?
26
- start
27
- end
28
+ def authenticate?
29
+ ! @options[:auth].nil? and ! @options[:auth].empty?
30
+ end
28
31
 
29
- if ! @options[:auth].nil? and ! @options[:auth].empty?
30
- @agent.add_auth(url, @options[:auth][:username], @options[:auth][:password])
32
+ def crawl?
33
+ @options[:crawl][:enabled]
34
+ end
35
+
36
+ def crawl(url)
37
+ start if @agent.nil?
38
+
39
+ links = []
40
+ @agent.add_auth(url, @options[:auth][:username], @options[:auth][:password]) if authenticate?
41
+ begin
42
+ page=@agent.get(url)
43
+ page=@agent.get(url) if authenticate?
44
+ page.links.each do |l|
45
+ @agent.log.debug("Link found: #{l.href}") if debug?
46
+ links << l.href
47
+ end
48
+ rescue Mechanize::UnauthorizedError
49
+ return {:status=>'KO', :links=>[], :message=>'target website requires authentication'}
50
+ rescue => e
51
+ return {:status=>'KO', :links=>links, :message=>e.to_s}
31
52
  end
32
53
 
54
+ return {:status=>'OK', :links=>links, :message=>''}
55
+ end
56
+
57
+ def inject(url)
58
+ start if @agent.nil?
59
+
60
+ $logger.log "Authenticating to the app using #{@options[:auth][:username]}:#{@options[:auth][:password]}" if debug?
61
+
62
+ @agent.add_auth(url, @options[:auth][:username], @options[:auth][:password]) if authenticate?
63
+
33
64
  found = false
34
65
  if @options[:exploit_url]
35
66
  # You ask to exploit the url, so I won't check for form values
36
67
 
68
+ attack_url = Cross::Url.new(url)
69
+
37
70
  Cross::Attack::XSS.each do |pattern|
38
- page = @agent.get(url+pattern)
71
+ attack_url.params.each do |par|
39
72
 
40
- if @options[:debug]
41
- @agent.log.debug(page.body)
42
- end
43
- scripts = page.search("//script")
44
- scripts.each do |sc|
45
- if sc.children.text.include?("alert('cross canary');")
46
- found = true
47
- end
48
- if @options[:debug]
49
- @agent.log.debug(sc.children.text)
73
+ page = @agent.get(attack_url.fuzz(par[:name],pattern))
74
+ @agent.log.debug(page.body) if debug?
75
+
76
+ scripts = page.search("//script")
77
+ scripts.each do |sc|
78
+ $logger.log(page.body) if @options[:debug] if sc.children.text.include?("alert('cross canary')")
79
+ return true if sc.children.text.include?("alert('cross canary')")
50
80
  end
51
- end
52
81
 
53
- puts "GET #{url+pattern}: #{found}"
82
+ return false if options[:oneshot]
83
+
84
+ attack_url.reset
85
+ end
54
86
  end
55
87
 
56
88
  else
57
- page = @agent.get(url)
58
- page.forms.each do |f|
59
- f.fields.each do |ff|
60
- ff.value = "<script>alert('cross canary');</script>"
61
- end
62
- pp = @agent.submit(f)
63
- scripts = pp.search("//script")
64
- scripts.each do |sc|
65
- if sc.children.text == "alert('cross canary');"
66
- found = true
89
+ begin
90
+ page = @agent.get(url)
91
+ rescue Mechanize::UnauthorizedError
92
+ $logger.err 'Authentication failed. Giving up.'
93
+ return false
94
+ rescue Mechanize::ResponseCodeError
95
+ $logger.err 'Server gave back 404. Giving up.'
96
+ return false
97
+ rescue Net::HTTP::Persistent::Error => e
98
+ $logger.err e.message
99
+ return false
100
+ end
101
+
102
+ $logger.log "#{page.forms.size} form(s) found" if debug?
103
+
104
+ Cross::Attack::XSS.each do |pattern|
105
+
106
+ $logger.log "using attack vector: #{pattern}" if debug?
107
+
108
+
109
+ page.forms.each do |f|
110
+ f.fields.each do |ff|
111
+ if options[:sample_post].empty?
112
+ ff.value = pattern if options[:parameter_to_tamper].empty?
113
+ ff.value = pattern if ! options[:parameter_to_tamper].empty? && ff.name==options[:parameter_to_tamper]
114
+ else
115
+ ff.value = find_sample_value_for(options[:sample_post], ff.name) unless ff.name==options[:parameter_to_tamper]
116
+ ff.value = pattern if ff.name==options[:parameter_to_tamper]
117
+
118
+
119
+ # promo=Promo1&codice=&nome=&cognome=&indirizzo=%3Cscript%3Ealert%28%27cross+canary%27%29%3C%2Fscript%3E&comune=&CAP=&provincia=&num1=&num2=&mail=&codfisc=&fase=1
120
+ end
67
121
  end
68
- end
69
- end
122
+
123
+ pp = @agent.submit(f)
124
+ $logger.log "header: #{pp.header}" if debug? && ! pp.header.empty?
125
+ $logger.log "body: #{pp.body}" if debug? && ! pp.body.empty?
126
+ $logger.err "Page is empty" if pp.body.empty?
127
+ scripts = pp.search("//script")
128
+ scripts.each do |sc|
129
+ return true if sc.children.text.include?("alert('cross canary')")
130
+ end
131
+ end
132
+ return false if options[:oneshot]
133
+ end
70
134
  end
71
135
  found
72
136
  end
73
137
 
138
+
139
+ private
140
+ def find_sample_value_for(sample, name)
141
+ v=sample.split('&')
142
+ v.each do |post_param|
143
+ post_param_v = post_param.split('=')
144
+ return post_param_v[1] if post_param_v[0] == name
145
+ end
146
+
147
+ return ""
148
+ end
74
149
  end
75
150
  end
data/lib/cross/url.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Cross
2
+ class Url
3
+
4
+ attr_reader :url
5
+ attr_reader :base_url
6
+ attr_reader :params
7
+ attr_reader :original_params
8
+
9
+ def initialize(url)
10
+ @url = url
11
+ @params = []
12
+ @original_params = []
13
+ @base_url = url.split('?')[0]
14
+ p_array = url.split('?')[1].split('&')
15
+ p_array.each do |p|
16
+ pp = p.split('=')
17
+ param = {}
18
+ param[:name] = pp[0]
19
+ param[:value] = pp[1] unless pp[1].nil?
20
+
21
+ @params << param
22
+ @original_params << param.dup
23
+ end
24
+ @original_params.freeze
25
+ end
26
+
27
+ def to_s
28
+ "#{@base_url}?#{params_to_url}"
29
+ end
30
+
31
+ def fuzz(name, value)
32
+ set(name, value)
33
+ "#{@base_url}?#{params_to_url}"
34
+ end
35
+
36
+ def get(name)
37
+ value = nil
38
+ @params.each do |p|
39
+ value = p[:value] if p[:name] == name
40
+ end
41
+ value
42
+ end
43
+
44
+ def set(name, value)
45
+ @params.each do |p|
46
+ p[:value] = value if p[:name] == name
47
+ end
48
+ end
49
+
50
+ def reset
51
+ @params = []
52
+ @original_params.each do |p|
53
+ @params << p.dup
54
+ end
55
+ end
56
+
57
+ def params_to_url
58
+ ret = ""
59
+ @params.each do |p|
60
+ ret += "#{p[:name]}=#{p[:value]}"
61
+ if !(p == @params.last)
62
+ ret +="&"
63
+ end
64
+ end
65
+ ret
66
+
67
+ end
68
+ end
69
+ end
data/lib/cross/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cross
2
- VERSION = "0.30.0"
2
+ VERSION = "0.50.0"
3
3
  end
data/lib/cross/xss.rb CHANGED
@@ -5,11 +5,17 @@ module Cross
5
5
  def self.each
6
6
 
7
7
  evasions = [
8
+ "<script>alert('cross canary')</script>",
8
9
  "<script>alert('cross canary');</script>",
10
+ "/--><script>alert('cross canary')</script>",
9
11
  "/--><script>alert('cross canary');</script>",
12
+ "/--></ScRiPt><ScRiPt>alert('cross canary')</ScRiPt>",
10
13
  "/--></ScRiPt><ScRiPt>alert('cross canary');</ScRiPt>",
14
+ "//;-->alert('cross canary')",
11
15
  "//;-->alert('cross canary');",
16
+ "\"//;\nalert('cross canary')",
12
17
  "\"//;\nalert('cross canary');",
18
+ " onmouseover=alert('1');",
13
19
  # more exotic vectors (antisnatchor's collection)
14
20
  "<script/anyjunk>alert('cross canary')</script>",
15
21
  "<<script>alert('cross canary');//<</script>",
data/spec/url_spec.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Url fuzzer" do
4
+ let (:url) {Cross::Url.new("http://localhost:8080/WebGoat/attack?Screen=130&menu=900")}
5
+ it "will recognize the param list" do
6
+ url.params.class.should == Array
7
+ url.params.size.should == 2
8
+ end
9
+
10
+ it "will recognize parameter names" do
11
+ url.params[0][:name].should == 'Screen'
12
+ url.params[1][:name].should == 'menu'
13
+ end
14
+
15
+ it "will recognize parameter values" do
16
+ url.params[0][:value].should == "130"
17
+ url.params[1][:value].should == "900"
18
+ end
19
+
20
+ it "will provide a get shortcut for getting parameters value" do
21
+ url.get("Screen").should == "130"
22
+ url.get("menu").should == "900"
23
+ end
24
+
25
+ it "will handle errors smoothly" do
26
+ url.get("nonexistent").should be_nil
27
+ end
28
+
29
+ it "will make a copy of parameters" do
30
+ url.original_params.should == url.params
31
+ end
32
+
33
+ it "will make params Array to be reverted as string" do
34
+ url.params_to_url.should == "Screen=130&menu=900"
35
+ end
36
+
37
+ describe "will provide an handy set shortcut that" do
38
+ it "sets an existing params to a given value" do
39
+ url.set("Screen", "123")
40
+ url.get("Screen").should == "123"
41
+ end
42
+
43
+ it "handle the error condition smootly" do
44
+ url.set("nonexistent", false)
45
+ url.original_params.should == url.params
46
+ end
47
+
48
+ it "won't change the original params" do
49
+ url.set("Screen", "123")
50
+ url.original_params.should_not == url.params
51
+ end
52
+ end
53
+ it "will fuzz" do
54
+ url.fuzz("Screen", "12").should == "http://localhost:8080/WebGoat/attack?Screen=12&menu=900"
55
+ url.fuzz("Screen", "afuzztest").should == "http://localhost:8080/WebGoat/attack?Screen=afuzztest&menu=900"
56
+ url.fuzz("menu", "11").should == "http://localhost:8080/WebGoat/attack?Screen=afuzztest&menu=11"
57
+ end
58
+
59
+ it "will fuzz honoring original params if requested" do
60
+ url.reset
61
+ url.get("Screen").should == "130"
62
+ url.get("menu").should == "900"
63
+ url.fuzz("Screen", "12").should == "http://localhost:8080/WebGoat/attack?Screen=12&menu=900"
64
+ url.reset
65
+ url.get("Screen").should == "130"
66
+ url.get("menu").should == "900"
67
+ url.fuzz("Screen", "afuzztest").should == "http://localhost:8080/WebGoat/attack?Screen=afuzztest&menu=900"
68
+ url.reset
69
+ url.get("Screen").should == "130"
70
+ url.get("menu").should == "900"
71
+ url.fuzz("menu", "11").should == "http://localhost:8080/WebGoat/attack?Screen=130&menu=11"
72
+
73
+ end
74
+ end
metadata CHANGED
@@ -1,110 +1,111 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cross
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.0
5
- prerelease:
4
+ version: 0.50.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Paolo Perego
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-07-23 00:00:00.000000000 Z
11
+ date: 2013-10-11 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rake
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - '>='
28
25
  - !ruby/object:Gem::Version
29
26
  version: '0'
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rspec
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - '>='
36
32
  - !ruby/object:Gem::Version
37
33
  version: '0'
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - '>='
44
39
  - !ruby/object:Gem::Version
45
40
  version: '0'
46
41
  - !ruby/object:Gem::Dependency
47
42
  name: rest-open-uri
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
- - - ! '>='
45
+ - - '>='
52
46
  - !ruby/object:Gem::Version
53
47
  version: '0'
54
48
  type: :runtime
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
- - - ! '>='
52
+ - - '>='
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
62
55
  - !ruby/object:Gem::Dependency
63
56
  name: mechanize
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
- - - ! '>='
59
+ - - '>='
68
60
  - !ruby/object:Gem::Version
69
61
  version: '0'
70
62
  type: :runtime
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
- - - ! '>='
66
+ - - '>='
76
67
  - !ruby/object:Gem::Version
77
68
  version: '0'
78
69
  - !ruby/object:Gem::Dependency
79
70
  name: logger
80
71
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
72
  requirements:
83
- - - ! '>='
73
+ - - '>='
84
74
  - !ruby/object:Gem::Version
85
75
  version: '0'
86
76
  type: :runtime
87
77
  prerelease: false
88
78
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
79
  requirements:
91
- - - ! '>='
80
+ - - '>='
92
81
  - !ruby/object:Gem::Version
93
82
  version: '0'
94
83
  - !ruby/object:Gem::Dependency
95
84
  name: rainbow
96
85
  requirement: !ruby/object:Gem::Requirement
97
- none: false
98
86
  requirements:
99
- - - ! '>='
87
+ - - '>='
100
88
  - !ruby/object:Gem::Version
101
89
  version: '0'
102
90
  type: :runtime
103
91
  prerelease: false
104
92
  version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
93
  requirements:
107
- - - ! '>='
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: codesake-commons
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
108
109
  - !ruby/object:Gem::Version
109
110
  version: '0'
110
111
  description: cross is a cross site scripting testing tool
@@ -128,40 +129,36 @@ files:
128
129
  - cross.gemspec
129
130
  - lib/cross.rb
130
131
  - lib/cross/engine.rb
132
+ - lib/cross/url.rb
131
133
  - lib/cross/version.rb
132
134
  - lib/cross/xss.rb
133
135
  - spec/cross_spec.rb
134
136
  - spec/spec_helper.rb
137
+ - spec/url_spec.rb
135
138
  homepage: ''
136
139
  licenses: []
140
+ metadata: {}
137
141
  post_install_message:
138
142
  rdoc_options: []
139
143
  require_paths:
140
144
  - lib
141
145
  required_ruby_version: !ruby/object:Gem::Requirement
142
- none: false
143
146
  requirements:
144
- - - ! '>='
147
+ - - '>='
145
148
  - !ruby/object:Gem::Version
146
149
  version: '0'
147
- segments:
148
- - 0
149
- hash: 3227883298359843932
150
150
  required_rubygems_version: !ruby/object:Gem::Requirement
151
- none: false
152
151
  requirements:
153
- - - ! '>='
152
+ - - '>='
154
153
  - !ruby/object:Gem::Version
155
154
  version: '0'
156
- segments:
157
- - 0
158
- hash: 3227883298359843932
159
155
  requirements: []
160
156
  rubyforge_project:
161
- rubygems_version: 1.8.24
157
+ rubygems_version: 2.0.4
162
158
  signing_key:
163
- specification_version: 3
159
+ specification_version: 4
164
160
  summary: cross is a cross site scripting testing tool
165
161
  test_files:
166
162
  - spec/cross_spec.rb
167
163
  - spec/spec_helper.rb
164
+ - spec/url_spec.rb