Last active
September 14, 2015 19:56
-
-
Save nruth/e6f052ea3349c80731fe to your computer and use it in GitHub Desktop.
ruby/rails user signup email screening with google safebrowsing api -- catch phishing domain typos in user email addresses
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
http_interactions: | |
- request: | |
method: get | |
uri: https://sb-ssl.google.com/safebrowsing/api/lookup?appver=1&client=your-app-name-here&key=abc123fakekey&pver=3.1&url=gmai.com | |
body: | |
encoding: US-ASCII | |
string: '' | |
headers: | |
Accept: | |
- "*/*; q=0.5, application/xml" | |
Accept-Encoding: | |
- gzip, deflate | |
Timeout: | |
- '4' | |
User-Agent: | |
- Ruby | |
response: | |
status: | |
code: 200 | |
message: OK | |
headers: | |
Content-Type: | |
- application/octet-stream | |
P3p: | |
- CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 | |
for more info." | |
X-Content-Type-Options: | |
- nosniff | |
Date: | |
- Wed, 05 Aug 2015 22:59:16 GMT | |
Server: | |
- HTTP server (unknown) | |
Content-Length: | |
- '7' | |
X-Xss-Protection: | |
- 1; mode=block | |
X-Frame-Options: | |
- SAMEORIGIN | |
Set-Cookie: | |
- NID=70=RqpUA10yjTPamIiNtmDoFuI7HMSg0TNISCuaU1SgRBpKY9ScNcBL6fHN7jaeynrEXevBm9WFlAgO3DRUQ1BOABzUAu6fZ-A-aGnzzmw8OAqdh-mi9ZlhmnqR8_XE7pF4; | |
expires=Thu, 04-Feb-2016 22:59:16 GMT; path=/; domain=.google.com; HttpOnly | |
- PREF=ID=1111111111111111:TM=1438815556:LM=1438815556:V=1:S=QbXwVGLqYBhLAblQ; | |
expires=Fri, 04-Aug-2017 22:59:16 GMT; path=/; domain=.google.com | |
Alternate-Protocol: | |
- 443:quic,p=1 | |
Expires: | |
- Wed, 05 Aug 2015 22:59:16 GMT | |
Cache-Control: | |
- private | |
body: | |
encoding: UTF-8 | |
string: malware | |
http_version: | |
recorded_at: Wed, 05 Aug 2015 22:59:16 GMT | |
recorded_with: VCR 2.9.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rest-client' | |
# sometimes users give the wrong email address | |
# moreso since smartphone keyboards, errant auto-correct, and big players mixing their domains inconsistently by country. | |
# even worse for some apps, they might type in a domain that's incorrect, but has a phishing operator sat on it running an MX server to intercept any email | |
# help your users out: warn them if they've done something that looks supicious | |
# e.g. if they registered with someguy@gmai.com instead of gmail.com, chances are it's an error | |
# You may find confirmation emails solve this problem, but your users may get pretty confused waiting for ones that will never arrive, losing you sales. | |
class EmailScreener | |
attr_reader :email | |
def initialize(email) | |
@email = email | |
end | |
# is the given email address probably a data-entry error | |
# e.g. algo@gmai.com instead of algo@gmail.com | |
def suspects_domain_error? | |
!domain_exists? || phishing? | |
end | |
# return the address if it could be understood | |
def parseable? | |
begin | |
Mail::Address.new(email).address | |
rescue Mail::Field::ParseError | |
false | |
end | |
end | |
def username | |
@username ||= Mail::Address.new(email).local | |
end | |
def domain | |
@domain ||= Mail::Address.new(email).domain | |
end | |
# is there a mail exchange (MX) record (DNS) for this domain? | |
def domain_exists? | |
ValidateEmail.mx_valid?(email) | |
end | |
def warnings | |
@warnings ||= if %w(hotmail.com hotmail.co.uk).include?(domain) | |
'Please double-check whether your address is with hotmail.com or hotmail.co.uk' | |
end | |
end | |
# how does google safebrowsing consider this domain? | |
# Is it a gmail-typo-phishing-site? e.g. gmai.com | |
def phishing? | |
unless safebrowsing_api_key.present? | |
Rails.logger.error "Safebrowsing API key missing" | |
return false | |
end | |
begin | |
response = RestClient.get( | |
'https://sb-ssl.google.com/safebrowsing/api/lookup', | |
timeout: safebrowsing_timeout, | |
params: { | |
key: safebrowsing_api_key, | |
appver: '1', | |
client: 'your-app-name-here', | |
pver:'3.1', | |
url: domain | |
} | |
) | |
# 200 for malware or phishing, 204 not malware | |
# https://developers.google.com/safe-browsing/lookup_guide | |
response.code == 200 | |
rescue RestClient::Exception | |
Rails.logger.info "SafeBrowsing email check returned error #{e.code} for #{email}" | |
# can't tell, so just approve | |
false | |
rescue | |
Rails.logger.error "SafeBrowsing lookup unknown error #{e.message}" | |
# can't tell, so just approve | |
false | |
end | |
end | |
private | |
def safebrowsing_timeout | |
ENV['GOOGLE_SAFEBROWSING_TIMEOUT'] || 4 | |
end | |
def safebrowsing_api_key | |
ENV['GOOGLE_SAFEBROWSING_API_KEY'] | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- encoding : utf-8 -*- | |
require 'spec_helper' | |
describe "EmailScreener" do | |
describe 'phishing' do | |
it 'returns false when no API key present' do | |
expect(EmailScreener.new('foo@gmai.com')).not_to be_phishing | |
end | |
it 'suspects gmai.com when response says so (vcr/webmock)' do | |
original_key = ENV['GOOGLE_SAFEBROWSING_API_KEY'] | |
begin | |
ENV['GOOGLE_SAFEBROWSING_API_KEY'] = 'abc123fakekey' | |
VCR.use_cassette('email-screener-google-safebrowsing', record: :once) do | |
screener = EmailScreener.new('foo@gmai.com') | |
expect(screener).to be_phishing | |
end | |
ensure | |
ENV['GOOGLE_SAFEBROWSING_API_KEY'] = original_key | |
end | |
end | |
end | |
describe 'suspects_domain_error?' do | |
it 'true for phishing' do | |
screener = EmailScreener.new('foo@gmai.com') | |
allow(screener).to receive(:phishing?).and_return true | |
expect(screener.suspects_domain_error?).to be true | |
end | |
it 'true for mx not found' do | |
expect(EmailScreener.new('foo@gmaix2kdlkjf.co.uk').suspects_domain_error?).to be true | |
end | |
end | |
describe 'parseable?' do | |
it 'returns the address if it could be read' do | |
expect(EmailScreener.new('foo@bar.net').parseable?).to eq('foo@bar.net') | |
end | |
it 'returns false if it couldnt understand the address' do | |
expect(EmailScreener.new('foo@bar.').parseable?).to be false | |
end | |
end | |
describe 'warnings' do | |
it 'warns hotmail users to check .com or .co.uk' do | |
hotmail_warning = 'Please double-check whether your address is with hotmail.com or hotmail.co.uk' | |
expect(EmailScreener.new('foo@hotmail.com').warnings).to eq(hotmail_warning) | |
expect(EmailScreener.new('foo@hotmail.co.uk').warnings).to eq(hotmail_warning) | |
end | |
end | |
describe 'username' do | |
it 'returns the username before @' do | |
expect(EmailScreener.new('foo@hotmail.com').username).to eq('foo') | |
end | |
it 'fails when address not parsable' do | |
end | |
end | |
describe 'domain' do | |
it 'returns the mailhost after @' do | |
expect(EmailScreener.new('foo@hotmail.com').domain).to eq('hotmail.com') | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment