kopia lustrzana https://github.com/dgtlmoon/changedetection.io
Security update - Use CSRF token protection for forms, make "remove password" use HTTP Post (#484)
rodzic
f2fa638480
commit
5483f5d694
|
@ -35,6 +35,7 @@ from flask import (
|
|||
url_for,
|
||||
)
|
||||
from flask_login import login_required
|
||||
from flask_wtf import CSRFProtect
|
||||
|
||||
from changedetectionio import html_tools
|
||||
|
||||
|
@ -72,6 +73,9 @@ app.config['LOGIN_DISABLED'] = False
|
|||
# Disables caching of the templates
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
notification_debug_log=[]
|
||||
|
||||
def init_app_secret(datastore_path):
|
||||
|
@ -610,16 +614,15 @@ def changedetection_app(config=None, datastore_o=None):
|
|||
form.notification_format.data = datastore.data['settings']['application']['notification_format']
|
||||
form.base_url.data = datastore.data['settings']['application']['base_url']
|
||||
|
||||
# Password unset is a GET, but we can lock the session to always need the password
|
||||
if not os.getenv("SALTED_PASS", False) and request.values.get('removepassword') == 'yes':
|
||||
from pathlib import Path
|
||||
if request.method == 'POST' and form.data.get('removepassword_button') == True:
|
||||
# Password unset is a GET, but we can lock the session to a salted env password to always need the password
|
||||
if not os.getenv("SALTED_PASS", False):
|
||||
datastore.data['settings']['application']['password'] = False
|
||||
flash("Password protection removed.", 'notice')
|
||||
flask_login.logout_user()
|
||||
return redirect(url_for('settings_page'))
|
||||
|
||||
if request.method == 'POST' and form.validate():
|
||||
|
||||
datastore.data['settings']['application']['notification_urls'] = form.notification_urls.data
|
||||
datastore.data['settings']['requests']['minutes_between_check'] = form.minutes_between_check.data
|
||||
datastore.data['settings']['application']['extract_title_as_title'] = form.extract_title_as_title.data
|
||||
|
|
|
@ -353,3 +353,5 @@ class globalSettingsForm(commonSettingsForm):
|
|||
global_subtractive_selectors = StringListField('Remove elements', [ValidateCSSJSONXPATHInput(allow_xpath=False, allow_json=False)])
|
||||
global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()])
|
||||
ignore_whitespace = BooleanField('Ignore whitespace')
|
||||
save_button = SubmitField('Save', render_kw={"class": "pure-button pure-button-primary"})
|
||||
removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"})
|
|
@ -19,6 +19,7 @@
|
|||
<div class="box-wrap inner">
|
||||
<form class="pure-form pure-form-stacked"
|
||||
action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next') ) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="tab-pane-inner" id="general">
|
||||
<fieldset>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<div class="edit-form">
|
||||
<div class="inner">
|
||||
<form class="pure-form pure-form-aligned" action="{{url_for('import_page')}}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<fieldset class="pure-group">
|
||||
<legend>
|
||||
Enter one URL per line, and optionally add tags for each URL after a space, delineated by comma (,):
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<div class="login-form">
|
||||
<div class="inner">
|
||||
<form class="pure-form pure-form-stacked" action="{{url_for('login')}}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
<label for="password">Password</label>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<div class="edit-form">
|
||||
<div class="box-wrap inner">
|
||||
<form class="pure-form pure-form-stacked" action="{{url_for('scrub_page')}}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
This will remove all version snapshots/data, but keep your list of URLs. <br/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% from '_helpers.jinja' import render_field %}
|
||||
{% from '_helpers.jinja' import render_field, render_button %}
|
||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
||||
|
||||
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='settings.js')}}" defer></script>
|
||||
|
@ -18,6 +18,7 @@
|
|||
</div>
|
||||
<div class="box-wrap inner">
|
||||
<form class="pure-form pure-form-stacked settings" action="{{url_for('settings_page')}}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="tab-pane-inner" id="general">
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
|
@ -27,8 +28,7 @@
|
|||
<div class="pure-control-group">
|
||||
{% if not hide_remove_pass %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{url_for('settings_page', removepassword='yes')}}"
|
||||
class="pure-button pure-button-primary">Remove password</a>
|
||||
{{ render_button(form.removepassword_button) }}
|
||||
{% else %}
|
||||
{{ render_field(form.password) }}
|
||||
<span class="pure-form-message-inline">Password protection for your changedetection.io application.</span>
|
||||
|
@ -114,11 +114,9 @@ nav
|
|||
|
||||
<div id="actions">
|
||||
<div class="pure-control-group">
|
||||
<button type="submit" class="pure-button pure-button-primary">Save</button>
|
||||
<a href="{{url_for('index')}}" class="pure-button button-small button-cancel">Back</a>
|
||||
<a href="{{url_for('scrub_page')}}" class="pure-button button-small button-cancel">Delete
|
||||
History
|
||||
Snapshot Data</a>
|
||||
{{ render_button(form.save_button) }}
|
||||
<a href="{{url_for('index')}}" class="pure-button button-small button-cancel">Back</a>
|
||||
<a href="{{url_for('scrub_page')}}" class="pure-button button-small button-cancel">Delete History Snapshot Data</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<div class="box">
|
||||
|
||||
<form class="pure-form" action="{{ url_for('api_watch_add') }}" method="POST" id="new-watch-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<fieldset>
|
||||
<legend>Add a new change detection watch</legend>
|
||||
{{ render_simple_field(form.url, placeholder="https://...", required=true) }}
|
||||
|
|
|
@ -42,6 +42,9 @@ def app(request):
|
|||
cleanup(app_config['datastore_path'])
|
||||
datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'], include_default_watches=False)
|
||||
app = changedetection_app(app_config, datastore)
|
||||
|
||||
# Disable CSRF while running tests
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
app.config['STOP_THREADS'] = True
|
||||
|
||||
def teardown():
|
||||
|
|
|
@ -4,8 +4,8 @@ from flask import url_for
|
|||
def test_check_access_control(app, client):
|
||||
# Still doesnt work, but this is closer.
|
||||
|
||||
with app.test_client() as c:
|
||||
# Check we dont have any password protection enabled yet.
|
||||
with app.test_client(use_cookies=True) as c:
|
||||
# Check we don't have any password protection enabled yet.
|
||||
res = c.get(url_for("settings_page"))
|
||||
assert b"Remove password" not in res.data
|
||||
|
||||
|
@ -46,15 +46,20 @@ def test_check_access_control(app, client):
|
|||
assert b"BACKUP" in res.data
|
||||
assert b"IMPORT" in res.data
|
||||
assert b"LOG OUT" in res.data
|
||||
assert b"minutes_between_check" in res.data
|
||||
assert b"fetch_backend" in res.data
|
||||
|
||||
# Now remove the password so other tests function, @todo this should happen before each test automatically
|
||||
res = c.get(url_for("settings_page", removepassword="yes"),
|
||||
follow_redirects=True)
|
||||
assert b"Password protection removed." in res.data
|
||||
|
||||
res = c.get(url_for("index"))
|
||||
assert b"LOG OUT" not in res.data
|
||||
|
||||
res = c.post(
|
||||
url_for("settings_page"),
|
||||
data={
|
||||
"minutes_between_check": 180,
|
||||
"tag": "",
|
||||
"headers": "",
|
||||
"fetch_backend": "html_webdriver",
|
||||
"removepassword_button": "Remove password"
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
# There was a bug where saving the settings form would submit a blank password
|
||||
def test_check_access_control_no_blank_password(app, client):
|
||||
|
@ -71,8 +76,7 @@ def test_check_access_control_no_blank_password(app, client):
|
|||
data={"password": "",
|
||||
"minutes_between_check": 180,
|
||||
'fetch_backend': "html_requests"},
|
||||
|
||||
follow_redirects=True
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert b"Password protection enabled." not in res.data
|
||||
|
@ -91,7 +95,8 @@ def test_check_access_no_remote_access_to_remove_password(app, client):
|
|||
# Enable password check.
|
||||
res = c.post(
|
||||
url_for("settings_page"),
|
||||
data={"password": "password", "minutes_between_check": 180,
|
||||
data={"password": "password",
|
||||
"minutes_between_check": 180,
|
||||
'fetch_backend': "html_requests"},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
@ -99,8 +104,17 @@ def test_check_access_no_remote_access_to_remove_password(app, client):
|
|||
assert b"Password protection enabled." in res.data
|
||||
assert b"Login" in res.data
|
||||
|
||||
res = c.get(url_for("settings_page", removepassword="yes"),
|
||||
follow_redirects=True)
|
||||
res = c.post(
|
||||
url_for("settings_page"),
|
||||
data={
|
||||
"minutes_between_check": 180,
|
||||
"tag": "",
|
||||
"headers": "",
|
||||
"fetch_backend": "html_webdriver",
|
||||
"removepassword_button": "Remove password"
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"Password protection removed." not in res.data
|
||||
|
||||
res = c.get(url_for("index"),
|
||||
|
|
|
@ -25,6 +25,7 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
|||
data={"urls": url_for('test_endpoint', _external=True)},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert b"1 Imported" in res.data
|
||||
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
flask~= 2.0
|
||||
|
||||
flask_wtf
|
||||
eventlet>=0.31.0
|
||||
validators
|
||||
timeago ~=1.0
|
||||
|
|
Ładowanie…
Reference in New Issue