mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-11-04 00:27:48 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			403-soluti
			...
			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