mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-10-31 06:37:41 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			parallel-d
			...
			security-u
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 344f25b412 | ||
|   | 3702973c7f | ||
|   | b8286c829a | ||
|   | dc1594b04f | 
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user