CVE-2015-10072: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: A vulnerability classified as problematic was found in NREL api-umbrella-web 0.7.1. This vulnerability affects unknown code of the component Flash Message Handler. The manipulation leads to cross site scripting. The attack can be initiated remotely. Upgrading to version 0.8.0 is able to address this issue. The name of the patch is bcc0e922c61d30367678c8f17a435950969315cd. It is recommended to upgrade the affected component. The identifier of this vulnerability is VDB-220060.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-02-04T04:15Z
Repository: https://github.com/NREL/api-umbrella-web
Language: Ruby
Files changed: Gemfile, Gemfile.lock, app/assets/javascripts/admin/controllers/admin_groups/form_controller.js, app/assets/javascripts/admin/controllers/admins/form_controller.js, app/assets/javascripts/admin/controllers/api_scopes/form_controller.js, app/assets/javascripts/admin/controllers/api_users/form_controller.js, app/assets/javascripts/admin/controllers/apis_form_controller.js, app/assets/javascripts/admin/controllers/website_backends/form_controller.js, spec/cassettes/elasticsearch_templates.yml, spec/controllers/admin/stats_controller_spec.rb, spec/factories/api_users.rb, spec/factories/log_items.rb, spec/features/admin/api_users_spec.rb, spec/features/admin/stats_logs_spec.rb, spec/features/admin/stats_users_spec.rb, spec/support/elasticsearch.rb, spec/support/vcr.rb
Commit date: 2015-04-10 11:30:01 -0600
Commit message: Fix additional XSS issues in the flash messages in the admin.

A follow-up to XSS issues first identified in
f53a9fb87e10c457f0f3dd4f2af24d3b2f21b3ca.

I've gone through the rest of the admin, and I believe this covers
everything.

This commit is a bit bigger, since it fixes some issues with how our
test elasticsearch database was being setup for this web app (the
database didn't have our actual mappings loaded, so some fields weren't
being indexes as the proper types). These test fixes allow us to now
properly test the analytics by users list, where some additional XSS
tests have been added.

Diff: diff --git a/Gemfile b/Gemfile
index 2700e07..5e227ab 100644
--- a/Gemfile
+++ b/Gemfile
@@ -243,10 +243,14 @@ group :development, :test do
   # For creating test data in elasticsearch.
   gem "elasticsearch-persistence", "~> 0.1.6"
 
   # For parsing URLs in tests.
   gem "addressable", "~> 2.3.6"
+
+  # For caching external web requests
+  gem "vcr", "~> 2.9.3", :require => false
+  gem "webmock", "~> 1.21.0", :require => false
 end
 
 group :development do
   # Deployment
   gem "capistrano", "~> 3.3.5"
diff --git a/Gemfile.lock b/Gemfile.lock
index a684fe5..2db2eb4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -120,10 +120,12 @@ GEM
       multi_json (~> 1.3)
       rest-client (= 1.6.7)
       simplecov (>= 0.7)
       term-ansicolor (= 1.2.2)
       thor (= 0.18.1)
+    crack (0.4.2)
+      safe_yaml (~> 1.0.0)
     css_parser (1.3.5)
       addressable
     csv_builder (2.1.1)
       actionpack (>= 3.0.0)
     currencies (0.4.2)
@@ -473,17 +475,21 @@ GEM
     tzinfo (0.3.42)
     uglifier (2.5.3)
       execjs (>= 0.3.0)
       json (>= 1.8.0)
     uuidtools (2.1.5)
+    vcr (2.9.3)
     virtus (1.0.4)
       axiom-types (~> 0.1)
       coercible (~> 1.0)
       descendants_tracker (~> 0.0, >= 0.0.3)
       equalizer (~> 0.0, >= 0.0.9)
     warden (1.2.3)
       rack (>= 1.0)
+    webmock (1.21.0)
+      addressable (>= 2.3.6)
+      crack (>= 0.3.2)
     websocket-driver (0.4.0)
     websocket-driver (0.4.0-java)
     xpath (2.0.0)
       nokogiri (~> 1.3)
 
@@ -586,5 +592,7 @@ DEPENDENCIES
   tabs_on_rails (~> 2.2.0)
   therubyracer (~> 0.12.1)
   turbo-sprockets-rails3 (= 0.3.13)
   uglifier (~> 2.5.0)
   uuidtools (~> 2.1.4)
+  vcr (~> 2.9.3)
+  webmock (~> 1.21.0)
diff --git a/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js b/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js
index fd01146..c2dd582 100644
--- a/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_groups/form_controller.js
@@ -9,11 +9,11 @@ Admin.AdminGroupsFormController = Ember.ObjectController.extend(Admin.Save, {
 
   actions: {
     submit: function() {
       this.save({
         transitionToRoute: 'admin_groups',
-        message: 'Successfully saved the admin group "' + this.get('model.name') + '"',
+        message: 'Successfully saved the admin group "' + _.escape(this.get('model.name')) + '"',
       });
     },
   },
 });
 
diff --git a/app/assets/javascripts/admin/controllers/admins/form_controller.js b/app/assets/javascripts/admin/controllers/admins/form_controller.js
index b0893c4..967ce81 100644
--- a/app/assets/javascripts/admin/controllers/admins/form_controller.js
+++ b/app/assets/javascripts/admin/controllers/admins/form_controller.js
@@ -9,11 +9,11 @@ Admin.AdminsFormController = Ember.ObjectController.extend(Admin.Save, {
 
   actions: {
     submit: function() {
       this.save({
         transitionToRoute: 'admins',
-        message: 'Successfully saved the admin "' + this.get('model.username') + '"',
+        message: 'Successfully saved the admin "' + _.escape(this.get('model.username')) + '"',
       });
     },
   },
 });
 
diff --git a/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js b/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js
index cdb8bcc..a639ada 100644
--- a/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js
+++ b/app/assets/javascripts/admin/controllers/api_scopes/form_controller.js
@@ -1,11 +1,11 @@
 Admin.ApiScopesFormController = Ember.ObjectController.extend(Admin.Save, {
   actions: {
     submit: function() {
       this.save({
         transitionToRoute: 'api_scopes',
-        message: 'Successfully saved the API scope "' + this.get('model.name') + '"',
+        message: 'Successfully saved the API scope "' + _.escape(this.get('model.name')) + '"',
       });
     },
   },
 });
 
diff --git a/app/assets/javascripts/admin/controllers/api_users/form_controller.js b/app/assets/javascripts/admin/controllers/api_users/form_controller.js
index ac4cd52..a87ef3e 100644
--- a/app/assets/javascripts/admin/controllers/api_users/form_controller.js
+++ b/app/assets/javascripts/admin/controllers/api_users/form_controller.js
@@ -18,13 +18,13 @@ Admin.ApiUsersFormController = Ember.ObjectController.extend(Admin.Save, {
   actions: {
     submit: function() {
       this.save({
         transitionToRoute: 'api_users',
         message: function(model) {
-          var message = 'Successfully saved the user "' + model.get('email') + '"';
+          var message = 'Successfully saved the user "' + _.escape(model.get('email')) + '"';
           if(model.get('apiKey')) {
-            message += '<br>API Key: <code>' + model.get('apiKey') + '</code>';
+            message += '<br>API Key: <code>' + _.escape(model.get('apiKey')) + '</code>';
           }
 
           return message;
         },
       });
diff --git a/app/assets/javascripts/admin/controllers/apis_form_controller.js b/app/assets/javascripts/admin/controllers/apis_form_controller.js
index 996845e..ae646dd 100644
--- a/app/assets/javascripts/admin/controllers/apis_form_controller.js
+++ b/app/assets/javascripts/admin/controllers/apis_form_controller.js
@@ -19,11 +19,11 @@ Admin.ApisFormController = Ember.ObjectController.extend(Admin.Save, {
 
   actions: {
     submit: function() {
       this.save({
         transitionToRoute: 'apis',
-        message: 'Successfully saved the "' + this.get('model.name') + '" API backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
+        message: 'Successfully saved the "' + _.escape(this.get('model.name')) + '" API backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
       });
     },
 
     delete: function() {
       bootbox.confirm('Are you sure you want to delete this API backend?', _.bind(function(result) {
diff --git a/app/assets/javascripts/admin/controllers/website_backends/form_controller.js b/app/assets/javascripts/admin/controllers/website_backends/form_controller.js
index 7c639bf..bf758a4 100644
--- a/app/assets/javascripts/admin/controllers/website_backends/form_controller.js
+++ b/app/assets/javascripts/admin/controllers/website_backends/form_controller.js
@@ -20,11 +20,11 @@ Admin.WebsiteBackendsFormController = Ember.ObjectController.extend(Admin.Save,
 
   actions: {
     submit: function() {
       this.save({
         transitionToRoute: 'website_backends',
-        message: 'Successfully saved the "' + this.get('model.frontendHost') + '" website backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
+        message: 'Successfully saved the "' + _.escape(this.get('model.frontendHost')) + '" website backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
       });
     },
 
     delete: function() {
       bootbox.confirm('Are you sure you want to delete this website backend?', _.bind(function(result) {
diff --git a/spec/cassettes/elasticsearch_templates.yml b/spec/cassettes/elasticsearch_templates.yml
new file mode 100644
index 0000000..d56fed3
--- /dev/null
+++ b/spec/cassettes/elasticsearch_templates.yml
@@ -0,0 +1,221 @@
+---
+http_interactions:
+- request:
+    method: get
+    uri: https://raw.githubusercontent.com/NREL/api-umbrella-router/master/config/elasticsearch_templates.json
+    body:
+      encoding: US-ASCII
+      string: ''
+    headers:
+      Accept:
+      - "*/*; q=0.5, application/xml"
+      Accept-Encoding:
+      - gzip, deflate
+      User-Agent:
+      - Ruby
+  response:
+    status:
+      code: 200
+      message: OK
+    headers:
+      Date:
+      - Fri, 10 Apr 2015 17:17:38 GMT
+      Server:
+      - Apache
+      Access-Control-Allow-Origin:
+      - https://render.githubusercontent.com
+      Content-Security-Policy:
+      - default-src 'none'
+      X-Xss-Protection:
+      - 1; mode=block
+      X-Frame-Options:
+      - deny
+      X-Content-Type-Options:
+      - nosniff
+      Strict-Transport-Security:
+      - max-age=31536000
+      Etag:
+      - '"d426630bd78408d97d4ae86d0d7a7c224849d203"'
+      Content-Type:
+      - text/plain; charset=utf-8
+      Cache-Control:
+      - max-age=300
+      Content-Length:
+      - '3937'
+      Accept-Ranges:
+      - bytes
+      Via:
+      - 1.1 varnish
+      X-Served-By:
+      - cache-mia1323-MIA
+      X-Cache:
+      - MISS
+      X-Cache-Hits:
+      - '0'
+      Vary:
+      - Authorization,Accept-Encoding
+      Expires:
+      - Fri, 10 Apr 2015 17:22:38 GMT
+      Source-Age:
+      - '0'
+    body:
+      encoding: ASCII-8BIT
+      string: |
+        [
+          {
+            "id": "api-umbrella-template",
+            "template": {
+              "template": "api-umbrella",
+              "mappings": {
+                "city": {
+                  "_all": {
+                    "enabled": false
+                  },
+                  "_timestamp": {
+                    "enabled": true,
+                    "path": "updated_at"
+                  },
+                  "properties": {
+                    "country": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "region": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "city": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "location": {
+                      "type": "geo_point",
+                      "lat_lon": true
+                    }
+                  }
+                }
+              }
+            }
+          },
+          {
+            "id": "api-umbrella-log-v1-template",
+            "template": {
+              "template": "api-umbrella-logs-v1-*",
+              "settings": {
+                "index": {
+                  "number_of_shards": 3
+                },
+                "analysis": {
+                  "analyzer": {
+                    "keyword_lowercase": {
+                      "type": "custom",
+                      "tokenizer": "keyword",
+                      "filter": ["lowercase"]
+                    },
+                    "path_hierarchy_lowercase": {
+                      "type": "custom",
+                      "tokenizer": "path_hierarchy",
+                      "filter": ["lowercase"]
+                    }
+                  }
+                }
+              },
+              "mappings": {
+                "log": {
+                  "_all": {
+                    "enabled": false
+                  },
+                  "_timestamp": {
+                    "enabled": true,
+                    "path": "request_at"
+                  },
+                  "date_detection": false,
+                  "numeric_detection": false,
+                  "dynamic_templates": [
+                    {
+                      "string_template": {
+                        "match": "*",
+                        "match_mapping_type": "string",
+                        "mapping": {
+                          "type": "string",
+                          "index": "analyzed",
+                          "analyzer": "keyword_lowercase"
+                        }
+                      }
+                    }
+                  ],
+                  "properties": {
+                    "api_key": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "user_id": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "request_at": {
+                      "type": "date"
+                    },
+                    "request_ip": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "request_ip_country": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "request_ip_region": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "request_ip_city": {
+                      "type": "string",
+                      "index": "not_analyzed"
+                    },
+                    "request_path_hierarchy": {
+                      "type": "string",
+                      "analyzer": "path_hierarchy_lowercase"
+                    },
+                    "request_size": {
+                      "type": "integer"
+                    },
+                    "response_status": {
+                      "type": "short"
+                    },
+                    "response_age": {
+                      "type": "integer"
+                    },
+                    "response_content_length": {
+                      "type": "integer"
+                    },
+                    "response_size": {
+                      "type": "integer"
+                    },
+                    "response_time": {
+                      "type": "integer"
+                    },
+                    "backend_response_time": {
+                      "type": "integer"
+                    },
+                    "proxy_overhead": {
+                      "type": "integer"
+                    },
+                    "internal_response_time": {
+                      "type": "float"
+                    },
+                    "internal_gatekeeper_time": {
+                      "type": "float"
+                    },
+                    "request_ip_location": {
+                      "type": "geo_point",
+                      "lat_lon": true
+                    }
+                  }
+                }
+              }
+            }
+          }
+        ]
+    http_version: 
+  recorded_at: Fri, 10 Apr 2015 07:01:09 GMT
+recorded_with: VCR 2.9.3
diff --git a/spec/controllers/admin/stats_controller_spec.rb b/spec/controllers/admin/stats_controller_spec.rb
index eecba02..2cd8635 100644
--- a/spec/controllers/admin/stats_controller_spec.rb
+++ b/spec/controllers/admin/stats_controller_spec.rb
@@ -2,14 +2,15 @@ require 'spec_helper'
 
 describe Admin::StatsController do
   login_admin
 
   before(:each) do
-    begin
-      LogItem.gateway.client.indices.delete :index => LogItem.index_name
-    rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions
-    end
+    LogItem.gateway.client.delete_by_query :index => LogItem.index_name, :body => {
+      :query => {
+        :match_all => {},
+      },
+    }
   end
 
   describe "GET logs" do
     it "downloads a CSV that requires an elasticsearch scan and scroll query" do
       FactoryGirl.create_list(:log_item, 1005, :request_at => Time.parse("2015-01-16T06:06:28.816Z"))
diff --git a/spec/factories/api_users.rb b/spec/factories/api_users.rb
index 48068fb..07974c5 100644
--- a/spec/factories/api_users.rb
+++ b/spec/factories/api_users.rb
@@ -11,10 +11,11 @@ FactoryGirl.define do
     factory :invalid_api_user do
       terms_and_conditions ""
     end
 
     factory :xss_api_user do
+      email 'a@"><script class="xss-test">alert("Hello first_name");</script>.com'
       first_name '"><script class="xss-test">alert("Hello first_name");</script>'
       last_name '"><script class="xss-test">alert("Hello last_name");</script>'
       use_description '"><script class="xss-test">alert("Hello use_description");</script>'
       registration_source '"><script class="xss-test">alert("Hello registration_source");</script>'
     end
diff --git a/spec/factories/log_items.rb b/spec/factories/log_items.rb
index 04c6958..c12e515 100644
--- a/spec/factories/log_items.rb
+++ b/spec/factories/log_items.rb
@@ -1,17 +1,18 @@
 require "elasticsearch/persistence/model"
 
 class LogItem
   include Elasticsearch::Persistence::Model
 
-  index_name "api-umbrella-logs-2015-01"
+  index_name "api-umbrella-logs-write-2015-01"
+  document_type "log"
 
   attribute :api_key, String
-  attribute :backend_response_time, Float
+  attribute :backend_response_time, Integer
   attribute :internal_gatekeeper_time, Float
   attribute :internal_response_time, Float
-  attribute :proxy_overhead, Float
+  attribute :proxy_overhead, Integer
   attribute :request_accept_encoding, String
   attribute :request_at, Time
   attribute :request_hierarchy, Array
   attribute :request_host, String
   attribute :request_ip, String
@@ -31,11 +32,11 @@ class LogItem
   attribute :response_content_length, Integer
   attribute :response_content_type, String
   attribute :response_server, String
   attribute :response_size, Integer
   attribute :response_status, Integer
-  attribute :response_time, Float
+  attribute :response_time, Integer
   attribute :user_email, String
   attribute :user_id, String
   attribute :user_registration_source, String
 
   def save!
diff --git a/spec/features/admin/api_users_spec.rb b/spec/features/admin/api_users_spec.rb
index 8867a3e..11e7ca3 100644
--- a/spec/features/admin/api_users_spec.rb
+++ b/spec/features/admin/api_users_spec.rb
@@ -11,26 +11,36 @@ describe "api users form", :js => true do
 
     it "escapes html entities in the table" do
       visit "/admin/#/api_users"
 
       page.should have_content(@user.email)
-      page.should_not have_selector(".xss-test", :visible => :all)
       page.should have_content(@user.first_name)
       page.should have_content(@user.last_name)
       page.should have_content(@user.use_description)
       page.should have_content(@user.registration_source)
+      page.should_not have_selector(".xss-test", :visible => :all)
     end
 
     it "escapes html entities in the form" do
       visit "/admin/#/api_users/#{@user.id}/edit"
 
       find_field("E-mail").value.should eql(@user.email)
-      page.should_not have_selector(".xss-test", :visible => :all)
       find_field("First Name").value.should eql(@user.first_name)
       find_field("Last Name").value.should eql(@user.last_name)
       find_field("Purpose").value.should eql(@user.use_description)
       page.should have_content(@user.registration_source)
+      page.should_not have_selector(".xss-test", :visible => :all)
+    end
+
+    it "escapes html entities in flash confirmation message" do
+      visit "/admin/#/api_users/#{@user.id}/edit"
+
+      fill_in "Last Name", :with => "Doe"
+      click_button("Save")
+
+      page.should have_content("Successfully saved the user \"#{@user.email}\"")
+      page.should_not have_selector(".xss-test", :visible => :all)
     end
   end
 
   describe "api key in the save notification" do
     it "shows the api key when creating a new account" do
diff --git a/spec/features/admin/stats_logs_spec.rb b/spec/features/admin/stats_logs_spec.rb
index 0dfe0f5..6c63f12 100644
--- a/spec/features/admin/stats_logs_spec.rb
+++ b/spec/features/admin/stats_logs_spec.rb
@@ -1,34 +1,35 @@
 require "spec_helper"
 require "addressable/uri"
 
-describe "apis", :js => true do
+describe "analytics filter logs", :js => true do
   login_admin
 
   before(:each) do
-    begin
-      LogItem.gateway.client.indices.delete :index => LogItem.index_name
-    rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions
-    end
+    LogItem.gateway.client.delete_by_query :index => LogItem.index_name, :body => {
+      :query => {
+        :match_all => {},
+      },
+    }
   end
 
   describe "xss" do
     it "escapes html entities in the table" do
       log = FactoryGirl.create(:xss_log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :request_method => "OPTIONS")
       LogItem.gateway.refresh_index!
 
       visit "/admin/#/stats/logs/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day"
 
       page.should have_content(log.request_method)
-      page.should_not have_selector(".xss-test", :visible => :all)
       page.should have_content(log.request_accept_encoding)
       page.should have_content(log.request_ip_city)
       page.should have_content(log.request_ip_country)
       page.should have_content(log.request_ip_region)
       page.should have_content(log.request_user_agent)
       page.should have_content(log.response_content_type)
       page.should have_content(log.user_email)
+      page.should_not have_selector(".xss-test", :visible => :all)
     end
   end
 
   describe "csv download" do
     it "updates the download link as the query parameters change" do
diff --git a/spec/features/admin/stats_users_spec.rb b/spec/features/admin/stats_users_spec.rb
new file mode 100644
index 0000000..95ef44a
--- /dev/null
+++ b/spec/features/admin/stats_users_spec.rb
@@ -0,0 +1,29 @@
+require "spec_helper"
+
+describe "analytics by users", :js => true do
+  login_admin
+
+  before(:each) do
+    LogItem.gateway.client.delete_by_query :index => LogItem.index_name, :body => {
+      :query => {
+        :match_all => {},
+      },
+    }
+  end
+
+  describe "xss" do
+    it "escapes html entities in the table" do
+      user = FactoryGirl.create(:xss_api_user)
+      FactoryGirl.create(:xss_log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :api_key => user.api_key, :user_id => user.id, :user_email => user.email, :user_registration_source => user.registration_source)
+      LogItem.gateway.refresh_index!
+
+      visit "/admin/#/stats/users/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day"
+
+      page.should have_content(user.email)
+      page.should have_content(user.first_name)
+      page.should have_content(user.last_name)
+      page.should have_content(user.use_description)
+      page.should_not have_selector(".xss-test", :visible => :all)
+    end
+  end
+end
diff --git a/spec/support/elasticsearch.rb b/spec/support/elasticsearch.rb
new file mode 100644
index 0000000..5f971b7
--- /dev/null
+++ b/spec/support/elasticsearch.rb
@@ -0,0 +1,32 @@
+require "support/vcr"
+
+client = Elasticsearch::Client.new
+
+# Fetch the elasticsearch template file from the router project. Cache it with
+# VCR, but periodically re-record it to make sure we stay up-to-date.
+VCR.use_cassette("elasticsearch_templates", :re_record_interval => 1.day) do
+  templates = MultiJson.load(RestClient.get("https://raw.githubusercontent.com/NREL/api-umbrella-router/master/config/elasticsearch_templates.json"))
+  templates.each do |template|
+    client.indices.put_template({
+      :name => template["id"],
+      :body => template["template"],
+    })
+  end
+end
+
+# For simplicity sake, we're assuming our tests only deal with the 2015-01
+# index currently. First delete any existing indexes.
+%w(api-umbrella-logs-v1-2015-01 api-umbrella-logs-2015-01 api-umbrella-logs-write-2015-01).each do |index_name|
+  begin
+    client.indices.delete :index => index_name
+  rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions
+  end
+end
+
+# Create the index with proper aliases setup.
+client.indices.create(:index => "api-umbrella-logs-v1-2015-01", :body => {
+  :aliases => {
+    "api-umbrella-logs-2015-01" => {},
+    "api-umbrella-logs-write-2015-01" => {},
+  },
+})
diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb
new file mode 100644
index 0000000..3324ac9
--- /dev/null
+++ b/spec/support/vcr.rb
@@ -0,0 +1,20 @@
+require "vcr"
+
+VCR.configure do |c|
+  c.cassette_library_dir = "#{::Rails.root}/spec/cassettes"
+  c.hook_into :webmock
+  c.default_cassette_options = {
+    :record => :new_episodes,
+
+    # Store gzip responses as plaintext in the YAML.
+    :decode_compressed_response => true,
+
+    # Allow the same response to be used multiple times in a single test.
+    :allow_playback_repeats => true,
+  }
+
+  # Allow localhost connections for ElasticSearch.
+  c.ignore_localhost = true
+
+  c.configure_rspec_metadata!
+end

Vulnerable code:         message: 'Successfully saved the admin group "' + this.get('model.name') + '"',
        message: 'Successfully saved the admin "' + this.get('model.username') + '"',
        message: 'Successfully saved the API scope "' + this.get('model.name') + '"',
          var message = 'Successfully saved the user "' + model.get('email') + '"';
            message += '<br>API Key: <code>' + model.get('apiKey') + '</code>';
        message: 'Successfully saved the "' + this.get('model.name') + '" API backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
        message: 'Successfully saved the "' + this.get('model.frontendHost') + '" website backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
    begin
      LogItem.gateway.client.indices.delete :index => LogItem.index_name
    rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions
    end
  index_name "api-umbrella-logs-2015-01"
  attribute :backend_response_time, Float
  attribute :proxy_overhead, Float
  attribute :response_time, Float
      page.should_not have_selector(".xss-test", :visible => :all)
      page.should_not have_selector(".xss-test", :visible => :all)
describe "apis", :js => true do
    begin
      LogItem.gateway.client.indices.delete :index => LogItem.index_name
    rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions
    end
      page.should_not have_selector(".xss-test", :visible => :all)
Fixed code: 
  # For caching external web requests
  gem "vcr", "~> 2.9.3", :require => false
  gem "webmock", "~> 1.21.0", :require => false
    crack (0.4.2)
      safe_yaml (~> 1.0.0)
    vcr (2.9.3)
    webmock (1.21.0)
      addressable (>= 2.3.6)
      crack (>= 0.3.2)
  vcr (~> 2.9.3)
  webmock (~> 1.21.0)
        message: 'Successfully saved the admin group "' + _.escape(this.get('model.name')) + '"',
        message: 'Successfully saved the admin "' + _.escape(this.get('model.username')) + '"',
        message: 'Successfully saved the API scope "' + _.escape(this.get('model.name')) + '"',
          var message = 'Successfully saved the user "' + _.escape(model.get('email')) + '"';
            message += '<br>API Key: <code>' + _.escape(model.get('apiKey')) + '</code>';
        message: 'Successfully saved the "' + _.escape(this.get('model.name')) + '" API backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
        message: 'Successfully saved the "' + _.escape(this.get('model.frontendHost')) + '" website backend<br><strong>Note:</strong> Your changes are not yet live. <a href="/admin/#/config/publish">Publish Changes</a> to send your updates live.',
---
http_interactions:
- request:
    method: get
    uri: https://raw.githubusercontent.com/NREL/api-umbrella-router/master/config/elasticsearch_templates.json
    body:
      encoding: US-ASCII
      string: ''
    headers:
      Accept:
      - "*/*; q=0.5, application/xml"
      Accept-Encoding:
      - gzip, deflate
      User-Agent:
      - Ruby
  response:
    status:
      code: 200
      message: OK
    headers:
      Date:
      - Fri, 10 Apr 2015 17:17:38 GMT
      Server:
      - Apache
      Access-Control-Allow-Origin:
      - https://render.githubusercontent.com
      Content-Security-Policy:
      - default-src 'none'
      X-Xss-Protection:
      - 1; mode=block
      X-Frame-Options:
      - deny
      X-Content-Type-Options:
      - nosniff
      Strict-Transport-Security:
      - max-age=31536000
      Etag:
      - '"d426630bd78408d97d4ae86d0d7a7c224849d203"'
      Content-Type:
      - text/plain; charset=utf-8
      Cache-Control:
      - max-age=300
      Content-Length:
      - '3937'
      Accept-Ranges:
      - bytes
      Via:
      - 1.1 varnish
      X-Served-By:
      - cache-mia1323-MIA
      X-Cache:
      - MISS
      X-Cache-Hits:
      - '0'
      Vary:
      - Authorization,Accept-Encoding
      Expires:
      - Fri, 10 Apr 2015 17:22:38 GMT
      Source-Age:
      - '0'
    body:
      encoding: ASCII-8BIT
      string: |
        [
          {
            "id": "api-umbrella-template",
            "template": {
              "template": "api-umbrella",
              "mappings": {
                "city": {
                  "_all": {
                    "enabled": false
                  },
                  "_timestamp": {
                    "enabled": true,
                    "path": "updated_at"
                  },
                  "properties": {
                    "country": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "region": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "city": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "location": {
                      "type": "geo_point",
                      "lat_lon": true
                    }
                  }
                }
              }
            }
          },
          {
            "id": "api-umbrella-log-v1-template",
            "template": {
              "template": "api-umbrella-logs-v1-*",
              "settings": {
                "index": {
                  "number_of_shards": 3
                },
                "analysis": {
                  "analyzer": {
                    "keyword_lowercase": {
                      "type": "custom",
                      "tokenizer": "keyword",
                      "filter": ["lowercase"]
                    },
                    "path_hierarchy_lowercase": {
                      "type": "custom",
                      "tokenizer": "path_hierarchy",
                      "filter": ["lowercase"]
                    }
                  }
                }
              },
              "mappings": {
                "log": {
                  "_all": {
                    "enabled": false
                  },
                  "_timestamp": {
                    "enabled": true,
                    "path": "request_at"
                  },
                  "date_detection": false,
                  "numeric_detection": false,
                  "dynamic_templates": [
                    {
                      "string_template": {
                        "match": "*",
                        "match_mapping_type": "string",
                        "mapping": {
                          "type": "string",
                          "index": "analyzed",
                          "analyzer": "keyword_lowercase"
                        }
                      }
                    }
                  ],
                  "properties": {
                    "api_key": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "user_id": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "request_at": {
                      "type": "date"
                    },
                    "request_ip": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "request_ip_country": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "request_ip_region": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "request_ip_city": {
                      "type": "string",
                      "index": "not_analyzed"
                    },
                    "request_path_hierarchy": {
                      "type": "string",
                      "analyzer": "path_hierarchy_lowercase"
                    },
                    "request_size": {
                      "type": "integer"
                    },
                    "response_status": {
                      "type": "short"
                    },
                    "response_age": {
                      "type": "integer"
                    },
                    "response_content_length": {
                      "type": "integer"
                    },
                    "response_size": {
                      "type": "integer"
                    },
                    "response_time": {
                      "type": "integer"
                    },
                    "backend_response_time": {
                      "type": "integer"
                    },
                    "proxy_overhead": {
                      "type": "integer"
                    },
                    "internal_response_time": {
                      "type": "float"
                    },
                    "internal_gatekeeper_time": {
                      "type": "float"
                    },
                    "request_ip_location": {
                      "type": "geo_point",
                      "lat_lon": true
                    }
                  }
                }
              }
            }
          }
        ]
    http_version: 
  recorded_at: Fri, 10 Apr 2015 07:01:09 GMT
recorded_with: VCR 2.9.3
    LogItem.gateway.client.delete_by_query :index => LogItem.index_name, :body => {
      :query => {
        :match_all => {},
      },
    }
      email 'a@"><script class="xss-test">alert("Hello first_name");</script>.com'
  index_name "api-umbrella-logs-write-2015-01"
  document_type "log"
  attribute :backend_response_time, Integer
  attribute :proxy_overhead, Integer
  attribute :response_time, Integer
      page.should_not have_selector(".xss-test", :visible => :all)
      page.should_not have_selector(".xss-test", :visible => :all)
    end

    it "escapes html entities in flash confirmation message" do
      visit "/admin/#/api_users/#{@user.id}/edit"

      fill_in "Last Name", :with => "Doe"
      click_button("Save")

      page.should have_content("Successfully saved the user \"#{@user.email}\"")
      page.should_not have_selector(".xss-test", :visible => :all)
describe "analytics filter logs", :js => true do
    LogItem.gateway.client.delete_by_query :index => LogItem.index_name, :body => {
      :query => {
        :match_all => {},
      },
    }
      page.should_not have_selector(".xss-test", :visible => :all)
require "spec_helper"

describe "analytics by users", :js => true do
  login_admin

  before(:each) do
    LogItem.gateway.client.delete_by_query :index => LogItem.index_name, :body => {
      :query => {
        :match_all => {},
      },
    }
  end

  describe "xss" do
    it "escapes html entities in the table" do
      user = FactoryGirl.create(:xss_api_user)
      FactoryGirl.create(:xss_log_item, :request_at => Time.parse("2015-01-16T06:06:28.816Z"), :api_key => user.api_key, :user_id => user.id, :user_email => user.email, :user_registration_source => user.registration_source)
      LogItem.gateway.refresh_index!

      visit "/admin/#/stats/users/tz=America%2FDenver&search=&start_at=2015-01-12&end_at=2015-01-18&interval=day"

      page.should have_content(user.email)
      page.should have_content(user.first_name)
      page.should have_content(user.last_name)
      page.should have_content(user.use_description)
      page.should_not have_selector(".xss-test", :visible => :all)
    end
  end
end
require "support/vcr"

client = Elasticsearch::Client.new

# Fetch the elasticsearch template file from the router project. Cache it with
# VCR, but periodically re-record it to make sure we stay up-to-date.
VCR.use_cassette("elasticsearch_templates", :re_record_interval => 1.day) do
  templates = MultiJson.load(RestClient.get("https://raw.githubusercontent.com/NREL/api-umbrella-router/master/config/elasticsearch_templates.json"))
  templates.each do |template|
    client.indices.put_template({
      :name => template["id"],
      :body => template["template"],
    })
  end
end

# For simplicity sake, we're assuming our tests only deal with the 2015-01
# index currently. First delete any existing indexes.
%w(api-umbrella-logs-v1-2015-01 api-umbrella-logs-2015-01 api-umbrella-logs-write-2015-01).each do |index_name|
  begin
    client.indices.delete :index => index_name
  rescue Elasticsearch::Transport::Transport::Errors::NotFound # rubocop:disable Lint/HandleExceptions
  end
end

# Create the index with proper aliases setup.
client.indices.create(:index => "api-umbrella-logs-v1-2015-01", :body => {
  :aliases => {
    "api-umbrella-logs-2015-01" => {},
    "api-umbrella-logs-write-2015-01" => {},
  },
})
require "vcr"

VCR.configure do |c|
  c.cassette_library_dir = "#{::Rails.root}/spec/cassettes"
  c.hook_into :webmock
  c.default_cassette_options = {
    :record => :new_episodes,

    # Store gzip responses as plaintext in the YAML.
    :decode_compressed_response => true,

    # Allow the same response to be used multiple times in a single test.
    :allow_playback_repeats => true,
  }

  # Allow localhost connections for ElasticSearch.
  c.ignore_localhost = true

  c.configure_rspec_metadata!
end