Bots that prefetch should not destroy the secret (#100)

* Create preview view, remove sneaky-user-agents logic

* unit tests

* rename openSecret to viewSecret

* code clean-up and style

* rename view secret to reveal secret

* update authors list

* bump version to 1.5.0
This commit is contained in:
Lauri Lubi 2019-03-05 16:47:07 +01:00 committed by Jon Parise
parent d4c96cf58a
commit 1ac262e34e
9 changed files with 59 additions and 39 deletions

View file

@ -2,4 +2,5 @@
files = setup.py files = setup.py
commit = True commit = True
tag = True tag = True
current_version = 0.1.0 current_version = 1.5.0

View file

@ -19,3 +19,4 @@ Thanks a lot for the contributions of:
* Donny Winston * Donny Winston
* James Barclay * James Barclay
* Thomas Decaux * Thomas Decaux
* Lauri Lubi

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.4.0 current_version = 1.5.0
commit = True commit = True
tag = True tag = True
files = setup.py snappass/__init__.py files = setup.py snappass/__init__.py

View file

@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name='snappass', name='snappass',
version='1.4.1', version='1.5.0',
description="It's like SnapChat... for Passwords.", description="It's like SnapChat... for Passwords.",
long_description=(open('README.rst').read() + '\n\n' + long_description=(open('README.rst').read() + '\n\n' +
open('AUTHORS.rst').read()), open('AUTHORS.rst').read()),

View file

@ -1 +1 @@
__version__ = '1.4.1' __version__ = '1.5.0'

View file

@ -11,10 +11,6 @@ from redis.exceptions import ConnectionError
from werkzeug.urls import url_quote_plus from werkzeug.urls import url_quote_plus
from werkzeug.urls import url_unquote_plus from werkzeug.urls import url_unquote_plus
SNEAKY_USER_AGENTS = ('Slackbot', 'facebookexternalhit', 'Twitterbot',
'Facebot', 'WhatsApp', 'SkypeUriPreview', 'Iframely')
SNEAKY_USER_AGENTS_RE = re.compile('|'.join(SNEAKY_USER_AGENTS))
NO_SSL = os.environ.get('NO_SSL', False) NO_SSL = os.environ.get('NO_SSL', False)
TOKEN_SEPARATOR = '~' TOKEN_SEPARATOR = '~'
@ -127,6 +123,11 @@ def get_password(token):
return password.decode('utf-8') return password.decode('utf-8')
@check_redis_alive
def password_exists(token):
storage_key, decryption_key = parse_token(token)
return redis_client.exists(storage_key)
def empty(value): def empty(value):
if not value: if not value:
return True return True
@ -150,14 +151,6 @@ def clean_input():
return TIME_CONVERSION[time_period], request.form['password'] return TIME_CONVERSION[time_period], request.form['password']
def request_is_valid(request):
"""
Ensure the request validates the following:
- not made by some specific User-Agents (to avoid chat's preview feature issue)
"""
return not SNEAKY_USER_AGENTS_RE.search(request.headers.get('User-Agent', ''))
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def index(): def index():
return render_template('set_password.html') return render_template('set_password.html')
@ -177,9 +170,16 @@ def handle_password():
@app.route('/<password_key>', methods=['GET']) @app.route('/<password_key>', methods=['GET'])
def show_password(password_key): def preview_password(password_key):
if not request_is_valid(request): password_key = url_unquote_plus(password_key)
if not password_exists(password_key):
abort(404) abort(404)
return render_template('preview.html')
@app.route('/<password_key>', methods=['POST'])
def show_password(password_key):
password_key = url_unquote_plus(password_key) password_key = url_unquote_plus(password_key)
password = get_password(password_key) password = get_password(password_key)
if not password: if not password:

View file

@ -0,0 +1,10 @@
(function () {
$('#revealSecret').click(function () {
var form = $('<form/>')
.attr('id', 'revealSecretForm')
.attr('method', 'post');
form.appendTo($('body'));
form.submit();
});
})();

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<section>
<div class="page-header">
<h1>Secret</h1>
</div>
<p class="lead">You can only reveal the secret once!</p>
<div class="row">
<div class="col-sm-6 margin-bottom-10">
<button id="revealSecret" type="button" class="btn-lg btn-primary">Reveal secret</button>
</div>
</div>
</section>
</div>
{% endblock %}
{% block js %}
<script src="{{ config.STATIC_URL }}/clipboardjs/clipboard.min.js"></script>
<script src="{{ config.STATIC_URL }}/snappass/scripts/clipboard_button.js"></script>
<script src="{{ config.STATIC_URL }}/snappass/scripts/preview.js"></script>
{% endblock %}

View file

@ -106,32 +106,17 @@ class SnapPassRoutesTestCase(TestCase):
snappass.app.config['TESTING'] = True snappass.app.config['TESTING'] = True
self.app = snappass.app.test_client() self.app = snappass.app.test_client()
def test_show_password(self): def test_preview_password(self):
password = "I like novelty kitten statues!" password = "I like novelty kitten statues!"
key = snappass.set_password(password, 30) key = snappass.set_password(password, 30)
rv = self.app.get('/{0}'.format(key)) rv = self.app.get('/{0}'.format(key))
self.assertIn(password, rv.get_data(as_text=True)) self.assertNotIn(password, rv.get_data(as_text=True))
def test_bots_denial(self): def test_show_password(self):
""" password = "I like novelty kitten statues!"
Main known bots User-Agent should be denied access
"""
password = "Bots can't access this"
key = snappass.set_password(password, 30) key = snappass.set_password(password, 30)
a_few_sneaky_bots = [ rv = self.app.post('/{0}'.format(key))
"Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)", self.assertIn(password, rv.get_data(as_text=True))
"facebookexternalhit/1.1",
"Facebot/1.0",
"Twitterbot/1.0",
"_WhatsApp/2.12.81 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00",
"WhatsApp/2.16.6/i",
"SkypeUriPreview Preview/0.5",
"Iframely/0.8.5 (+http://iframely.com/;)",
]
for ua in a_few_sneaky_bots:
rv = self.app.get('/{0}'.format(key), headers={'User-Agent': ua})
self.assertEqual(404, rv.status_code)
if __name__ == '__main__': if __name__ == '__main__':