* most of VRFS-3674 (detect browser-based fraud) done

This commit is contained in:
Seth Call 2015-10-23 14:42:51 -05:00
parent b2fca8cc0b
commit f002864a08
24 changed files with 469 additions and 28 deletions

View File

@ -0,0 +1,13 @@
ActiveAdmin.register JamRuby::IpBlacklist, :as => 'IP Blacklist' do
menu :label => 'IP Blacklist', :parent => 'Operations'
config.sort_order = 'created_at desc'
config.batch_actions = false
index do
column :remote_ip
column :notes
column :created_at
end
end

View File

@ -0,0 +1,13 @@
ActiveAdmin.register JamRuby::UserBlacklist, :as => 'User Blacklist' do
menu :label => 'User Blacklist', :parent => 'Operations'
config.sort_order = 'created_at desc'
config.batch_actions = false
index do
column :user
column :notes
column :created_at
end
end

View File

@ -157,5 +157,9 @@ module JamAdmin
config.admin_port = ENV['ADMIN_PORT'] || 3333
config.admin_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.admin_port == 80 || config.admin_port == 443) ? '' : ':' + config.admin_port.to_s}"
config.download_tracker_day_range = 30
config.max_user_ip_address = 10
config.max_multiple_users_same_ip = 2
end
end

View File

@ -308,3 +308,4 @@ aac_master.sql
video_recording.sql
web_playable_jamtracks.sql
affiliate_partner_rate.sql
track_downloads.sql

30
db/up/track_downloads.sql Normal file
View File

@ -0,0 +1,30 @@
CREATE TABLE download_trackers (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
remote_ip VARCHAR(400) NOT NULL,
jam_track_id VARCHAR (64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
paid BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX index_download_trackers_on_user_id ON download_trackers USING btree (user_id);
CREATE INDEX index_download_trackers_on_remote_ip ON download_trackers USING btree (remote_ip);
CREATE INDEX index_download_trackers_on_created_at ON download_trackers USING btree (created_at, paid);
CREATE TABLE ip_blacklists (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
remote_ip VARCHAR(400) UNIQUE NOT NULL,
notes VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_blacklists (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
notes VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -111,6 +111,9 @@ require "jam_ruby/models/anonymous_user"
require "jam_ruby/models/signup_hint"
require "jam_ruby/models/machine_fingerprint"
require "jam_ruby/models/machine_extra"
require "jam_ruby/models/download_tracker"
require "jam_ruby/models/ip_blacklist"
require "jam_ruby/models/user_blacklist"
require "jam_ruby/models/fraud_alert"
require "jam_ruby/models/fingerprint_whitelist"
require "jam_ruby/models/rsvp_request"

View File

@ -0,0 +1,116 @@
module JamRuby
class DownloadTracker < ActiveRecord::Base
@@log = Logging.logger[DownloadTracker]
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :mixdown, :class_name => "JamRuby::JamTrackMixdownPackage", foreign_key: 'mixdown_id'
belongs_to :stem, :class_name => "JamRuby::JamTrackTrack", foreign_key: 'stem_id'
belongs_to :jam_track, :class_name => "JamRuby::JamTrack"
# one of mixdown or stem need to be specified. could validate this?
validates :user, presence:true
validates :remote_ip, presence: true
#validates :paid, presence: true
validates :jam_track, presence: :true
def self.create(user, remote_ip, target, owned)
dt = DownloadTracker.new
dt.user = user
dt.remote_ip = remote_ip
dt.paid = owned
if target.is_a?(JamTrackTrack)
dt.jam_track_id = target.jam_track_id
elsif target.is_a?(JamTrackMixdownPackage)
dt.jam_track_id = target.jam_track_mixdown.jam_track_id
end
if !dt.save
@@log.error("unable to create Download Tracker: #{dt.errors.inspect}")
end
dt
end
def self.check(user, remote_ip, target, owned)
create(user, remote_ip, target, owned)
# let's check the following
alert_freebies_snarfer(remote_ip)
alert_user_sharer(user)
end
# somebody who has shared account info with a large number of people
# high number of downloads of the same user from different IP addresses that were or were not paid for
# raw query created by this code:
# SELECT distinct(user_id), count(user_id) FROM "download_trackers" WHERE (created_at > NOW() - '30 days'::interval) GROUP BY user_id HAVING count(distinct(remote_ip)) >= 2
def self.check_user_sharer(max, user_id = nil)
query = DownloadTracker.select('distinct(user_id), count(user_id)')
query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval")
if !user_id.nil?
query = query.where('user_id = ?', user_id)
end
query.group(:user_id).having("count(distinct(remote_ip)) >= #{max}")
end
# somebody who has figured out how to bypass cookie based method of identity checking, and is getting lots of free JamTracks
# high number of downloads of different jam tracks from different users for the same IP address that weren't paid for
# raw query created by this code:
# SELECT distinct(remote_ip), count(remote_ip) FROM "download_trackers" WHERE (paid = false) AND (created_at > NOW() - '30 days'::interval) GROUP BY remote_ip HAVING count(distinct(jam_track_id)) >= 2
def self.check_freebie_snarfer(max, remote_ip = nil)
query = DownloadTracker.select('distinct(remote_ip), count(remote_ip)').where("paid = false")
query = query.where("created_at > NOW() - '#{APP_CONFIG.download_tracker_day_range} days'::interval")
if !remote_ip.nil?
query = query.where('remote_ip = ?', remote_ip)
end
query.group(:remote_ip).having("count(distinct(jam_track_id)) >= #{max}")
end
def self.alert_user_sharer(user)
violation = check_user_sharer(APP_CONFIG.max_user_ip_address, user.id).first
if violation
body = "User has downloaded from too many IP addresses #{user.id}\n"
body << "Download Count: #{violation['count']}\n"
body << "User URL #{user.admin_url}\n"
AdminMailer.alerts({
subject:"Account IP Access Violation. USER: #{user.email}",
body:body
}).deliver
end
end
def self.alert_freebies_snarfer(remote_ip)
violation = check_freebie_snarfer(APP_CONFIG.max_multiple_users_same_ip, remote_ip).first
if violation
body = "IP Address: #{remote_ip}\n"
body << "Download Count: #{violation['count']}\n"
AdminMailer.alerts({
subject:"Single IP Access Violation. IP:#{remote_ip}",
body:body
}).deliver
end
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/download_trackers/" + id
end
def to_s
if stem?
"stem:#{stem} #{remote_ip} #{user}"
else
"mixdown:#{mixdown} #{remote_ip} #{user}"
end
end
end
end

View File

@ -0,0 +1,18 @@
module JamRuby
class IpBlacklist < ActiveRecord::Base
attr_accessible :remote_ip, :notes, as: :admin
@@log = Logging.logger[IpBlacklist]
validates :remote_ip, presence:true, uniqueness:true
def self.listed(remote_ip)
IpBlacklist.count(:conditions => "remote_ip = '#{remote_ip}'") == 1
end
def to_s
remote_ip
end
end
end

View File

@ -0,0 +1,19 @@
module JamRuby
class UserBlacklist < ActiveRecord::Base
attr_accessible :user_id, :notes, as: :admin
@@log = Logging.logger[UserBlacklist]
belongs_to :user, :class_name => "JamRuby::User"
validates :user, presence:true, uniqueness: true
def self.listed(user)
UserBlacklist.count(:conditions => "user_id= '#{user.id}'") == 1
end
def to_s
user
end
end
end

View File

@ -790,6 +790,13 @@ FactoryGirl.define do
tap_in_count 3
end
factory :download_tracker, :class => JamRuby::DownloadTracker do
remote_ip '1.1.1.1'
paid false
association :user, factory: :user
association :jam_track, factory: :jam_track
end
factory :sale, :class => JamRuby::Sale do
order_total 0
association :user, factory:user

View File

@ -0,0 +1,162 @@
require 'spec_helper'
describe DownloadTracker do
let(:user1) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
let(:user3) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
describe "check_user_sharer" do
describe "with max 1" do
it "and there is one row that is not paid for" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_user_sharer(1)
results.all.count.should eq 1
results[0]['user_id'].should eql (user1.id)
end
end
describe "with max 2" do
it "and there is one row that is not paid for" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 0
# and then add that same user at different IP, and see that something shows up
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 1
end
it "and there are two rows from different IP address, different jam tracks" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 1
results[0]['user_id'].should eql(user1.id)
results[0]['count'].should eql('2')
# now add a second user with one of the previous IP addresses; shouldn't matter yet
tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 1
results[0]['user_id'].should eql(user1.id)
results[0]['count'].should eql('2')
end
it "and there are two rows from same IP adresss, same jam track" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 0
tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 0
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 1
results[0]['user_id'].should eql(user1.id)
results[0]['count'].should eql('3')
end
it "and there are two rows from same user one paid for, one not" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1')
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
results = DownloadTracker.check_user_sharer(2)
results.all.count.should eq 1
end
end
end
describe "check_freebie_snarfer" do
describe "with max 1" do
it "and there is one row that is not paid for" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(1)
results.all.count.should eq 1
end
it "and there is one row that is paid for" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(1)
results.all.count.should eq 0
end
end
describe "with max 2" do
it "and there is one row that is not paid for" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 0
# and then add a second user at same IP, and see that something shows up
tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 1
end
it "and there are two rows from same user, different jam tracks" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 1
results[0]['remote_ip'].should eql('1.1.1.1')
results[0]['count'].should eql('2')
tracker1 = FactoryGirl.create(:download_tracker, user: user2, paid: false, remote_ip: '2.2.2.2')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 1
results[0]['remote_ip'].should eql('1.1.1.1')
results[0]['count'].should eql('2')
end
it "and there are two rows from same user, same jam track" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1', jam_track: jam_track)
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 0
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '2.2.2.2')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 0
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 1
results[0]['remote_ip'].should eql('1.1.1.1')
results[0]['count'].should eql('3')
end
it "and there are two rows from same user one paid for, one not" do
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: true, remote_ip: '1.1.1.1')
tracker1 = FactoryGirl.create(:download_tracker, user: user1, paid: false, remote_ip: '1.1.1.1')
results = DownloadTracker.check_freebie_snarfer(2)
results.all.count.should eq 0
end
end
end
end

View File

@ -242,6 +242,10 @@ def app_config
300
end
def download_tracker_day_range
30
end
private
def audiomixer_workspace_path

View File

@ -133,7 +133,22 @@
app.notify({title: "Oops!", text: "What you were looking for is gone now."});
}
else if(jqXHR.status === 403) {
logger.debug("not logged in");
logger.debug("not logged in or something else");
try {
var responseJson = JSON.parse(jqXHR.responseText)
if (responseJson['message'] == 'IP blacklisted') {
app.notify({title: 'Suspicious Activity', text: 'The IP address you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
return;
}
if (responseJson['message'] == 'User blacklisted') {
app.notify({title: 'Suspicious Activity', text: 'The user account that you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
return;
}
}catch(e) { }
app.notify({title: 'Not Logged In', text: 'Please log in.', icon_url: "/assets/content/icon_alert_big.png"})
}
else if (jqXHR.status === 422) {
logger.error("Unexpected ajax error: " + textStatus + ", msg: " + errorMessage + ", response: " + jqXHR.responseText);
@ -230,6 +245,21 @@
}
else if(jqXHR.status == 403) {
logger.debug("permission error sent from server:", jqXHR.responseText)
try {
var responseJson = JSON.parse(jqXHR.responseText)
if (responseJson['message'] == 'IP blacklisted') {
this.notify({title: 'Suspicious Activity', text: 'The IP address you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
return;
}
if (responseJson['message'] == 'User blacklisted') {
this.notify({title: 'Suspicious Activity', text: 'The user account that you are accessing JamKazam from has shown suspicious activity in the past. If you believe this is in error, please contact support@jamkazam.com.', icon_url: "/assets/content/icon_alert_big.png"})
return;
}
}catch(e) { }
// default
this.notify({title: 'Permission Error', text: 'You do not have permission to access this information', icon_url: "/assets/content/icon_alert_big.png"})
}
else {

View File

@ -24,7 +24,7 @@ BrowserMediaActions = @BrowserMediaActions
# Register with the app store to get @app
this.listenTo(context.AppStore, this.onAppInit)
this.listenTo(context.BrowserMediaStore, this.onBrowserMediaChanged)
@browserMediaState = {}
onAppInit: (app) ->
@app = app

View File

@ -79,9 +79,7 @@
prepThanks();
})
.fail(function() {
})
.fail(app.ajaxError)
}
}

View File

@ -4,6 +4,8 @@ class ApiJamTrackMixdownsController < ApiController
before_filter :api_signed_in_user
before_filter :lookup_jam_track_mixdown, :only => [:download, :enqueue, :update]
before_filter :lookup_jam_track_right, :only => [:download, :enqueue, :update]
before_filter :ip_blacklist, :only => [:download]
before_filter :user_blacklist, :only => [:download]
respond_to :json
def log
@ -78,6 +80,7 @@ class ApiJamTrackMixdownsController < ApiController
end
if @package.ready?
DownloadTracker.check(current_user, request.remote_ip, @package, !@jam_track_right.redeemed)
@package.update_download_count
now = Time.now
@package.last_downloaded_at = now

View File

@ -4,6 +4,9 @@ class ApiJamTracksController < ApiController
before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :api_any_user, :only => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right, :mark_active, :download_stem]
before_filter :ip_blacklist, :only => [:download_stem, :download]
before_filter :user_blacklist, :only => [:download_stem, :download]
respond_to :json
@ -113,6 +116,8 @@ class ApiJamTracksController < ApiController
jam_track_track = JamTrackTrack.find(params[:stem_id])
end
DownloadTracker.check(current_user, request.remote_ip, @jam_track_track, !@jam_track_right.redeemed)
if params[:download]
redirect_to jam_track_track.web_download_sign_url(120, params[:file_type], 'application/octet-stream', "attachment; filename=\"#{@jam_track_right.jam_track.name + '-' + jam_track_track.display_name}.mp3\"")
else

View File

@ -2,6 +2,10 @@ require 'jam_ruby/recurly_client'
class ApiRecurlyController < ApiController
before_filter :api_signed_in_user, :except => [:create_account]
before_filter :create_client
before_filter :ip_blacklist, :only => [:place_order]
before_filter :user_blacklist, :only => [:place_order]
respond_to :json
# create Recurly account

View File

@ -15,6 +15,7 @@ ApiUsersController < ApiController
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy,
:share_session, :share_recording,
:affiliate_report, :audio_latency, :broadcast_notification]
before_filter :ip_blacklist, :only => [:create]
respond_to :json, :except => :calendar
respond_to :ics, :only => :calendar

View File

@ -21,6 +21,7 @@ class UsersController < ApplicationController
before_filter :correct_user, only: [:edit, :update]
before_filter :admin_user, only: :destroy
before_filter :is_native_client
before_filter :ip_blacklist, :only => [:create]
rescue_from 'JamRuby::PermissionError' do |exception|

View File

@ -139,6 +139,21 @@ module SessionsHelper
end
end
def ip_blacklist
if IpBlacklist.listed(request.remote_ip)
Stats.write('web.blacklist.ip', {value: 1, remote_ip: request.remote_ip})
render :json => { :message => "IP blacklisted"}, :status => 403
end
end
def user_blacklist
if UserBlacklist.listed(current_user)
Stats.write('web.blacklist.user', {value: 1, user_id: current_user.id})
render :json => { :message => "User blacklisted"}, :status => 403
end
end
# take either the signed in user, or if that fails, try the anonymous user
def api_any_user
unless signed_in? || has_anonymous_user?

View File

@ -370,5 +370,9 @@ if defined?(Bundler)
config.react.addons = true
config.time_shift_style = :sbsms # or sox
config.download_tracker_day_range = 30
config.max_user_ip_address = 10
config.max_multiple_users_same_ip = 2
end
end

View File

@ -833,31 +833,17 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
affiliate_params = partner.affiliate_query_params
visit '/landing/jamtracks/acdc-backinblack?' + affiliate_params
find('.browse-band a').trigger(:click)
find('h1', text: 'search jamtracks')
#find('a', text: 'What is a JamTrack?')
find("a.jamtrack-add-cart[data-jamtrack-id=\"#{jamtrack_acdc_backinblack.id}\"]", text: 'GET IT FREE!').trigger(:click)
find('h3', text: 'OR SIGN UP USING YOUR EMAIL')
shopping_carts = ShoppingCart.all
shopping_carts.count.should eq(1)
shopping_cart = shopping_carts[0]
shopping_cart.anonymous_user_id.should_not be_nil
shopping_cart.user_id.should be_nil
fill_in "email", with: 'testuser_12345@jamkazam.com'
fill_in "password", with: 'jam123'
find('.register-area ins').trigger(:click)
find('button.cta-button', text: 'GET IT FREE!').trigger(:click)
fill_in 'first_name', with: 'Seth'
fill_in 'last_name', with: 'Call'
fill_in 'email', with: 'guy_referral@jamkazam.com'
fill_in 'password', with: 'jam123'
find('.right-side .terms_of_service input').trigger(:click) # accept TOS
# this should show on the /client#/jamtrack page
find('.no-free-jamtrack')
# try to submit, and see order page
find('.signup-submit').trigger(:click)
find('.jam-tracks-in-browser')
guy = User.find_by_email('guy_referral@jamkazam.com')
guy = User.find_by_email('testuser_12345@jamkazam.com')
guy.affiliate_referral.should eq(partner)
guy.reload

View File

@ -122,6 +122,10 @@ def web_config
def num_packaging_nodes
2
end
def download_tracker_day_range
30
end
end
klass.new
end