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 [email protected] 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. [email protected] instead of [email protected] | |
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('[email protected]')).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('[email protected]') | |
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('[email protected]') | |
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('[email protected]').suspects_domain_error?).to be true | |
end | |
end | |
describe 'parseable?' do | |
it 'returns the address if it could be read' do | |
expect(EmailScreener.new('[email protected]').parseable?).to eq('[email protected]') | |
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('[email protected]').warnings).to eq(hotmail_warning) | |
expect(EmailScreener.new('[email protected]').warnings).to eq(hotmail_warning) | |
end | |
end | |
describe 'username' do | |
it 'returns the username before @' do | |
expect(EmailScreener.new('[email protected]').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('[email protected]').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