From c26e5e27205e5a308a4d2524d132e8e34a09546b Mon Sep 17 00:00:00 2001 From: chris18191 Date: Wed, 1 Jul 2020 13:27:14 +0200 Subject: [PATCH 1/5] Added check for only accessing config files in the nginx directory to prevent path traversal attacks. --- app/api/endpoints.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/api/endpoints.py b/app/api/endpoints.py index df8f9e1..b888a64 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -39,7 +39,12 @@ def post_config(name: str): content = flask.request.get_json() nginx_path = flask.current_app.config['NGINX_PATH'] - with io.open(os.path.join(nginx_path, name), 'w') as f: + config_file = os.path.join(nginx_path, name) + + if not os.path.commonprefix(os.path.realpath(config_file), nginx_path): + return flask.make_response({'success': False}), 200 + + with io.open(config_file, 'w') as f: f.write(content['file']) return flask.make_response({'success': True}), 200 @@ -57,6 +62,9 @@ def get_domains(): sites_available = [] sites_enabled = [] + if not os.path.exists(config_path): + errorMessage = 'The config folder "{}" does not exists.'.format(config_path) + return flask.render_template('domains.html', errorMessage=errorMessage, sites_available=(), sites_enabled=()), 200 for _ in os.listdir(config_path): if os.path.isfile(os.path.join(config_path, _)): From 4b16c9bd011ce437d16617fd6c6613eb9d59f4c3 Mon Sep 17 00:00:00 2001 From: chris18191 Date: Wed, 1 Jul 2020 13:29:30 +0200 Subject: [PATCH 2/5] Added entry for error handling on domains page in case that the config path does not exists to prevent internal error. --- app/templates/domains.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/templates/domains.html b/app/templates/domains.html index da8c91f..4c06616 100644 --- a/app/templates/domains.html +++ b/app/templates/domains.html @@ -1,6 +1,11 @@
+ {% if errorMessage %} +
+ {{errorMessage}} +
+ {% endif %} {% if sites_available %} {% for domain in sites_available %}
@@ -30,4 +35,4 @@
-
\ No newline at end of file +
From e55c8b8e001572c02da818a550f03f26dfc4f3ee Mon Sep 17 00:00:00 2001 From: chris18191 Date: Thu, 2 Jul 2020 13:55:05 +0200 Subject: [PATCH 3/5] added error rendering function and a function for getting valid paths. --- app/api/endpoints.py | 28 ++++++++++++++++++++++++++-- app/templates/error.html | 13 +++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 app/templates/error.html diff --git a/app/api/endpoints.py b/app/api/endpoints.py index b888a64..bd48cf5 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -5,6 +5,30 @@ from app.api import api +def renderError(msg, error_header="An error occured!", statusCode=500): + return flask.render_template('error.html', error_header=error_header, error_message=msg), statusCode + +def getValidPath(f: str, d: str): + """ + Returns the absolute path of wanted file or directory, but only if it is contained in the given directory. + + :param f: File name to retireve the path for. + :type f: str + + :param d: Directory, the file should be in. + :type d: str + + + :return: The valid path to access the file or None. + :rtype: str + """ + file_path = os.path.realpath(f) + common_path = os.path.commonpath((file_path, d)) + + if common_path == d: + return file_path + + return None @api.route('/config/', methods=['GET']) def get_config(name: str): @@ -63,8 +87,8 @@ def get_domains(): sites_enabled = [] if not os.path.exists(config_path): - errorMessage = 'The config folder "{}" does not exists.'.format(config_path) - return flask.render_template('domains.html', errorMessage=errorMessage, sites_available=(), sites_enabled=()), 200 + error_message = f'The config folder "{config_path}" does not exists.' + return renderError(error_message) for _ in os.listdir(config_path): if os.path.isfile(os.path.join(config_path, _)): diff --git a/app/templates/error.html b/app/templates/error.html new file mode 100644 index 0000000..2f5cb20 --- /dev/null +++ b/app/templates/error.html @@ -0,0 +1,13 @@ +
+ +
+ +
+ {{error_header}} +
+

+ {{error_message}} +

+
+ +
From f09f1d064e85057206c3bd7a2eab20b88ebafc00 Mon Sep 17 00:00:00 2001 From: chris18191 Date: Thu, 2 Jul 2020 13:56:07 +0200 Subject: [PATCH 4/5] added path filtering for other domains --- app/api/endpoints.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/api/endpoints.py b/app/api/endpoints.py index bd48cf5..d11f60c 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -8,11 +8,11 @@ def renderError(msg, error_header="An error occured!", statusCode=500): return flask.render_template('error.html', error_header=error_header, error_message=msg), statusCode -def getValidPath(f: str, d: str): +def getValidSubpath(f: str, d: str): """ Returns the absolute path of wanted file or directory, but only if it is contained in the given directory. - :param f: File name to retireve the path for. + :param f: File name or directory to retireve the path for. :type f: str :param d: Directory, the file should be in. @@ -25,6 +25,7 @@ def getValidPath(f: str, d: str): file_path = os.path.realpath(f) common_path = os.path.commonpath((file_path, d)) + # a valid sub path must contain the whole parent directory in its own path if common_path == d: return file_path @@ -43,7 +44,11 @@ def get_config(name: str): """ nginx_path = flask.current_app.config['NGINX_PATH'] - with io.open(os.path.join(nginx_path, name), 'r') as f: + path = getValidSubpath(os.path.join(nginx_path, name), nginx_path) + if path == None: + return renderError(f'Could not read file "{path}".') + + with io.open(path, 'r') as f: _file = f.read() return flask.render_template('config.html', name=name, file=_file), 200 @@ -65,8 +70,9 @@ def post_config(name: str): config_file = os.path.join(nginx_path, name) - if not os.path.commonprefix(os.path.realpath(config_file), nginx_path): - return flask.make_response({'success': False}), 200 + path = getValidSubpath(config_file, nginx_path) + if path == None: + return flask.make_response({'success': False}), 500 with io.open(config_file, 'w') as f: f.write(content['file']) @@ -162,8 +168,12 @@ def post_domain(name: str): new_domain = flask.render_template('new_domain.j2', name=name) name = name + '.conf.disabled' + path = getValidSubpath(os.path.join(config_path, name), config_path) + if path == None: + return flask.jsonify({'success': False, 'error_msg': 'invalid domain path'}), 500 + try: - with io.open(os.path.join(config_path, name), 'w') as f: + with io.open(path, 'w') as f: f.write(new_domain) response = flask.jsonify({'success': True}), 201 From dedde58c6ce9d2bc66954af2308befcd2afe4786 Mon Sep 17 00:00:00 2001 From: chris18191 Date: Thu, 2 Jul 2020 17:31:07 +0200 Subject: [PATCH 5/5] removed camel case notation --- app/api/endpoints.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/api/endpoints.py b/app/api/endpoints.py index d11f60c..5eeb0b7 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -5,10 +5,10 @@ from app.api import api -def renderError(msg, error_header="An error occured!", statusCode=500): +def render_error(msg, error_header="An error occured!", statusCode=500): return flask.render_template('error.html', error_header=error_header, error_message=msg), statusCode -def getValidSubpath(f: str, d: str): +def get_valid_subpath(f: str, d: str): """ Returns the absolute path of wanted file or directory, but only if it is contained in the given directory. @@ -44,9 +44,9 @@ def get_config(name: str): """ nginx_path = flask.current_app.config['NGINX_PATH'] - path = getValidSubpath(os.path.join(nginx_path, name), nginx_path) + path = get_valid_subpath(os.path.join(nginx_path, name), nginx_path) if path == None: - return renderError(f'Could not read file "{path}".') + return render_error(f'Could not read file "{path}".') with io.open(path, 'r') as f: _file = f.read() @@ -70,7 +70,7 @@ def post_config(name: str): config_file = os.path.join(nginx_path, name) - path = getValidSubpath(config_file, nginx_path) + path = get_valid_subpath(config_file, nginx_path) if path == None: return flask.make_response({'success': False}), 500 @@ -94,7 +94,7 @@ def get_domains(): if not os.path.exists(config_path): error_message = f'The config folder "{config_path}" does not exists.' - return renderError(error_message) + return render_error(error_message) for _ in os.listdir(config_path): if os.path.isfile(os.path.join(config_path, _)): @@ -168,7 +168,7 @@ def post_domain(name: str): new_domain = flask.render_template('new_domain.j2', name=name) name = name + '.conf.disabled' - path = getValidSubpath(os.path.join(config_path, name), config_path) + path = get_valid_subpath(os.path.join(config_path, name), config_path) if path == None: return flask.jsonify({'success': False, 'error_msg': 'invalid domain path'}), 500