public inbox for [email protected]
help / color / mirror / Atom feed[pgAdmin][5919] Fix security related issues
4+ messages / 3 participants
[nested] [flat]
* [pgAdmin][5919] Fix security related issues
@ 2020-10-19 12:01 Ganesh Jaybhay <[email protected]>
2020-10-19 12:08 ` Re: [pgAdmin][5919] Fix security related issues Dave Page <[email protected]>
0 siblings, 1 reply; 4+ messages in thread
From: Ganesh Jaybhay @ 2020-10-19 12:01 UTC (permalink / raw)
To: pgadmin-hackers
Hi Hackers,
Please find the attached patch to fix the below security issues:
- Host Header Injection - Added ALLOWED_HOSTS list to limit host address
- Lack of Content Security Policy (CSP) - Added security header
- Lack of Protection Mechanisms - HSTS - Added security header
- Lack of Cookie Attribute – Secure : Kept as False as secure limits
cookies to HTTPS traffic only.
- Information Disclosure – Web Server / Development Framework
VersionDescription: Kept as hard coded 'Python' instead of exposing
wsgi/python/gunicorn version info.
Please review and let me know if I have missed anything.
Regards,
Ganesh Jaybhay
Attachments:
[application/octet-stream] RM5919.patch (12.9K, 3-RM5919.patch)
download | inline diff:
diff --git a/Dockerfile b/Dockerfile
index 38c1310..3f5d504 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -81,7 +81,8 @@ RUN apk add --no-cache \
flask_gravatar \
flask_migrate \
simplejson \
- cryptography
+ cryptography \
+ netaddr
# Copy the docs from the local tree. Explicitly remove any existing builds that
# may be present
@@ -177,6 +178,7 @@ RUN ln -sf /usr/lib/libpq.so.5.12 /usr/lib/libpq.so.5
# Copy the runner script
COPY pkg/docker/run_pgadmin.py /pgadmin4
+COPY pkg/docker/gunicorn_config.py /pgadmin4
COPY pkg/docker/entrypoint.sh /entrypoint.sh
# Precompile and optimize python code to save time and space on startup
diff --git a/pkg/docker/entrypoint.sh b/pkg/docker/entrypoint.sh
index 5a482c7..93d809f 100755
--- a/pkg/docker/entrypoint.sh
+++ b/pkg/docker/entrypoint.sh
@@ -58,7 +58,7 @@ TIMEOUT=$(cd /pgadmin4 && python -c 'import config; print(config.SESSION_EXPIRAT
# Using --threads to have multi-threaded single-process worker
if [ ! -z ${PGADMIN_ENABLE_TLS} ]; then
- exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} --keyfile /certs/server.key --certfile /certs/server.cert run_pgadmin:app
+ exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
else
- exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} run_pgadmin:app
+ exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} -c gunicorn_config.py run_pgadmin:app
fi
diff --git a/pkg/docker/gunicorn_config.py b/pkg/docker/gunicorn_config.py
new file mode 100644
index 0000000..513c889
--- /dev/null
+++ b/pkg/docker/gunicorn_config.py
@@ -0,0 +1,2 @@
+import gunicorn
+gunicorn.SERVER_SOFTWARE = 'Python'
diff --git a/requirements.txt b/requirements.txt
index a5815a3..dbb0083 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -41,4 +41,5 @@ Flask-Security-Too>=3.0.0
bcrypt<=3.1.7
cryptography<=3.0
sshtunnel>=0.1.5
+netaddr==0.8.0
ldap3>=2.5.1
diff --git a/web/config.py b/web/config.py
index 702e73f..5d80a28 100644
--- a/web/config.py
+++ b/web/config.py
@@ -143,12 +143,53 @@ DEFAULT_SERVER = '127.0.0.1'
# environment by the runtime
DEFAULT_SERVER_PORT = 5050
+# This param is used to validate ALLOWED_HOSTS for the application
+# This will be used to avoid Host Header Injection attack
+# For how to set ALLOWED_HOSTS see netaddr library
+# For more details https://netaddr.readthedocs.io/en/latest/tutorial_03.html
+# e.g. ALLOWED_HOSTS = ['192.0.2.0/28', '::192.0.2.0/124']
+# ALLOWED_HOSTS = ['225.0.0.0/8', '226.0.0.0/7', '228.0.0.0/6']
+# ALLOWED_HOSTS = ['127.0.0.1', '192.168.0.1']
+# if ALLOWED_HOSTS= [] then it will accept all ips (and application will be
+# vulnerable to Host Header Injection attack)
+ALLOWED_HOSTS = []
+
+# This param is used to override the default web server information about
+# the web technology and the frameworks being used in the application
+# An attacker could use this information to fingerprint underlying operating
+# system and research known exploits for the specific version of
+# software in use
+WEB_SERVER = 'Python'
+
# Enable X-Frame-Option protection.
# Set to one of "SAMEORIGIN", "ALLOW-FROM origin" or "" to disable.
# Note that "DENY" is NOT supported (and will be silently ignored).
# See https://tools.ietf.org/html/rfc7034 for more info.
X_FRAME_OPTIONS = "SAMEORIGIN"
+# The Content-Security-Policy header allows you to restrict how resources
+# such as JavaScript, CSS, or pretty much anything that the browser loads.
+# see https://content-security-policy.com/#source_list for more info
+# e.g. "default-src https: data: 'unsafe-inline' 'unsafe-eval';"
+CONTENT_SECURITY_POLICY = "default-src http: data: blob: 'unsafe-inline' " \
+ "'unsafe-eval';"
+
+# The Strict-Transport-Security header tells the browser to convert all HTTP
+# requests to HTTPS, preventing man-in-the-middle (MITM) attacks.
+# e.g. 'max-age=31536000; includeSubDomains'
+STRICT_TRANSPORT_SECURITY = "max-age=31536000; includeSubDomains"
+
+# The X-Content-Type-Options header forces the browser to honor the response
+# content type instead of trying to detect it, which can be abused to
+# generate a cross-site scripting (XSS) attack.
+# e.g. nosniff
+X_CONTENT_TYPE_OPTIONS = "nosniff"
+
+# The browser will try to prevent reflected XSS attacks by not loading the
+# page if the request contains something that looks like JavaScript and the
+# response contains the same data. e.g. '1; mode=block'
+X_XSS_PROTECTION = "1; mode=block"
+
# Hashing algorithm used for password storage
SECURITY_PASSWORD_HASH = 'pbkdf2_sha512'
@@ -421,12 +462,14 @@ ON_DEMAND_RECORD_COUNT = 1000
SHOW_GRAVATAR_IMAGE = True
##########################################################################
-# Set cookie path
+# Set cookie path and options
##########################################################################
COOKIE_DEFAULT_PATH = '/'
COOKIE_DEFAULT_DOMAIN = None
SESSION_COOKIE_DOMAIN = None
SESSION_COOKIE_SAMESITE = 'Lax'
+SESSION_COOKIE_SECURE = False
+SESSION_COOKIE_HTTPONLY = True
#########################################################################
# Skip storing session in files and cache for specific paths
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index 275f365..59cab62 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -12,6 +12,7 @@ such as setup of logging, dynamic loading of modules etc."""
import logging
import os
import sys
+import re
from types import MethodType
from collections import defaultdict
from importlib import import_module
@@ -19,11 +20,13 @@ from importlib import import_module
from flask import Flask, abort, request, current_app, session, url_for
from werkzeug.exceptions import HTTPException
from flask_babelex import Babel, gettext
+from flask_babelex import gettext as _
from flask_login import user_logged_in, user_logged_out
from flask_mail import Mail
from flask_paranoid import Paranoid
from flask_security import Security, SQLAlchemyUserDatastore, current_user
from flask_security.utils import login_user, logout_user
+from netaddr import IPSet
from werkzeug.datastructures import ImmutableDict
from werkzeug.local import LocalProxy
from werkzeug.utils import find_modules
@@ -36,9 +39,10 @@ from pgadmin.utils.session import create_session_interface, pga_unauthorised
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
from datetime import timedelta
from pgadmin.setup import get_version, set_version
-from pgadmin.utils.ajax import internal_server_error
+from pgadmin.utils.ajax import internal_server_error, make_json_response
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin import authenticate
+from pgadmin.utils.security_headers import SecurityHeaders
winreg = None
if os.name == 'nt':
@@ -658,6 +662,36 @@ def create_app(app_name=None):
request.endpoint not in ('security.login', 'security.logout'):
logout_user()
+ @app.before_request
+ def limit_host_addr():
+ """
+ This function validate the hosts from ALLOWED_HOSTS before allowing
+ HTTP request to avoid Host Header Injection attack
+ :return: None/JSON response with 403 HTTP status code
+ """
+ client_host = str(request.host).split(':')[0]
+ valid = True
+ allowed_hosts = config.ALLOWED_HOSTS
+
+ if len(allowed_hosts) != 0:
+ regex = re.compile(
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2}|)')
+ # Create separate list for ip addresses and host names
+ ip_set = list(filter(lambda ip: regex.match(ip), allowed_hosts))
+ host_set = list(filter(lambda ip: not regex.match(ip),
+ allowed_hosts))
+ is_ip = regex.match(client_host)
+ if is_ip:
+ valid = IPSet(ip_set).__contains__(client_host)
+ else:
+ valid = host_set.__contains__(client_host)
+
+ if not valid:
+ return make_json_response(
+ status=403, success=0,
+ errormsg=_("403 FORBIDDEN")
+ )
+
@app.after_request
def after_request(response):
if 'key' in request.args:
@@ -667,13 +701,12 @@ def create_app(app_name=None):
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
response.set_cookie('PGADMIN_INT_KEY', value=request.args['key'],
path=config.COOKIE_DEFAULT_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
- # X-Frame-Options for security
- if config.X_FRAME_OPTIONS != "" and \
- config.X_FRAME_OPTIONS.lower() != "deny":
- response.headers["X-Frame-Options"] = config.X_FRAME_OPTIONS
-
+ SecurityHeaders.set_response_headers(response)
return response
##########################################################################
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 5375e94..6ed138b 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -697,6 +697,9 @@ def index():
response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
return response
diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py
index db11d1d..d05c1c8 100644
--- a/web/pgadmin/preferences/__init__.py
+++ b/web/pgadmin/preferences/__init__.py
@@ -232,6 +232,9 @@ def save(pid):
setattr(session, 'PGADMIN_LANGUAGE', language)
response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
return response
diff --git a/web/pgadmin/utils/security_headers.py b/web/pgadmin/utils/security_headers.py
new file mode 100644
index 0000000..c3cf31c
--- /dev/null
+++ b/web/pgadmin/utils/security_headers.py
@@ -0,0 +1,36 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+#########################################################################
+
+import config
+
+
+class SecurityHeaders:
+
+ @staticmethod
+ def set_response_headers(response):
+ """set response security headers"""
+
+ params_dict = {
+ 'CONTENT_SECURITY_POLICY': 'Content-Security-Policy',
+ 'STRICT_TRANSPORT_SECURITY': 'Strict-Transport-Security',
+ 'X_CONTENT_TYPE_OPTIONS': 'X-Content-Type-Options',
+ 'X_XSS_PROTECTION': 'X-XSS-Protection',
+ 'WEB_SERVER': 'Server',
+ }
+
+ # X-Frame-Options for security
+ if config.X_FRAME_OPTIONS != "" and \
+ config.X_FRAME_OPTIONS.lower() != "deny":
+ response.headers["X-Frame-Options"] = config.X_FRAME_OPTIONS
+
+ # add other security options
+ for key in params_dict:
+ if key in config.__dict__ and config.__dict__[key] != "" \
+ and config.__dict__[key] is not None:
+ response.headers[params_dict[key]] = config.__dict__[key]
diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py
index b8af3d4..62e03ba 100644
--- a/web/pgadmin/utils/session.py
+++ b/web/pgadmin/utils/session.py
@@ -311,7 +311,11 @@ class ManagedSessionInterface(SessionInterface):
response.set_cookie(
app.session_cookie_name,
'%s!%s' % (session.sid, session.hmac_digest),
- expires=cookie_exp, httponly=True, domain=domain
+ expires=cookie_exp,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
+ domain=domain
)
^ permalink raw reply [nested|flat] 4+ messages in thread
* Re: [pgAdmin][5919] Fix security related issues
2020-10-19 12:01 [pgAdmin][5919] Fix security related issues Ganesh Jaybhay <[email protected]>
@ 2020-10-19 12:08 ` Dave Page <[email protected]>
2020-10-19 13:46 ` Re: [pgAdmin][5919] Fix security related issues Ganesh Jaybhay <[email protected]>
0 siblings, 1 reply; 4+ messages in thread
From: Dave Page @ 2020-10-19 12:08 UTC (permalink / raw)
To: Ganesh Jaybhay <[email protected]>; +Cc: pgadmin-hackers
Hi
On Mon, Oct 19, 2020 at 1:01 PM Ganesh Jaybhay <
[email protected]> wrote:
> Hi Hackers,
>
> Please find the attached patch to fix the below security issues:
>
> - Host Header Injection - Added ALLOWED_HOSTS list to limit host
> address
> - Lack of Content Security Policy (CSP) - Added security header
> - Lack of Protection Mechanisms - HSTS - Added security header
> - Lack of Cookie Attribute – Secure : Kept as False as secure limits
> cookies to HTTPS traffic only.
> - Information Disclosure – Web Server / Development Framework
> VersionDescription: Kept as hard coded 'Python' instead of exposing
> wsgi/python/gunicorn version info.
>
> Please review and let me know if I have missed anything.
>
I took a very quick look at this, and one thing that immediately stood out
is that HSTS should definitely not be enabled by default. That can make
dev/test/redeploy extremely difficult.
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 4+ messages in thread
* Re: [pgAdmin][5919] Fix security related issues
2020-10-19 12:01 [pgAdmin][5919] Fix security related issues Ganesh Jaybhay <[email protected]>
2020-10-19 12:08 ` Re: [pgAdmin][5919] Fix security related issues Dave Page <[email protected]>
@ 2020-10-19 13:46 ` Ganesh Jaybhay <[email protected]>
2020-10-20 11:47 ` Re: [pgAdmin][5919] Fix security related issues Akshay Joshi <[email protected]>
0 siblings, 1 reply; 4+ messages in thread
From: Ganesh Jaybhay @ 2020-10-19 13:46 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: pgadmin-hackers
Thank you Dave for the suggestion.
Please find the attached updated patch to make HSTS by default disabled and
conditional based on flag.
Regards,
Ganesh Jaybhay
On Mon, Oct 19, 2020 at 5:38 PM Dave Page <[email protected]> wrote:
> Hi
>
> On Mon, Oct 19, 2020 at 1:01 PM Ganesh Jaybhay <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Please find the attached patch to fix the below security issues:
>>
>> - Host Header Injection - Added ALLOWED_HOSTS list to limit host
>> address
>> - Lack of Content Security Policy (CSP) - Added security header
>> - Lack of Protection Mechanisms - HSTS - Added security header
>> - Lack of Cookie Attribute – Secure : Kept as False as secure limits
>> cookies to HTTPS traffic only.
>> - Information Disclosure – Web Server / Development Framework
>> VersionDescription: Kept as hard coded 'Python' instead of exposing
>> wsgi/python/gunicorn version info.
>>
>> Please review and let me know if I have missed anything.
>>
>
> I took a very quick look at this, and one thing that immediately stood out
> is that HSTS should definitely not be enabled by default. That can make
> dev/test/redeploy extremely difficult.
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EDB: http://www.enterprisedb.com
>
>
Attachments:
[application/x-patch] RM5919_v1.patch (13.2K, 3-RM5919_v1.patch)
download | inline diff:
diff --git a/Dockerfile b/Dockerfile
index 38c1310..3f5d504 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -81,7 +81,8 @@ RUN apk add --no-cache \
flask_gravatar \
flask_migrate \
simplejson \
- cryptography
+ cryptography \
+ netaddr
# Copy the docs from the local tree. Explicitly remove any existing builds that
# may be present
@@ -177,6 +178,7 @@ RUN ln -sf /usr/lib/libpq.so.5.12 /usr/lib/libpq.so.5
# Copy the runner script
COPY pkg/docker/run_pgadmin.py /pgadmin4
+COPY pkg/docker/gunicorn_config.py /pgadmin4
COPY pkg/docker/entrypoint.sh /entrypoint.sh
# Precompile and optimize python code to save time and space on startup
diff --git a/pkg/docker/entrypoint.sh b/pkg/docker/entrypoint.sh
index 5a482c7..93d809f 100755
--- a/pkg/docker/entrypoint.sh
+++ b/pkg/docker/entrypoint.sh
@@ -58,7 +58,7 @@ TIMEOUT=$(cd /pgadmin4 && python -c 'import config; print(config.SESSION_EXPIRAT
# Using --threads to have multi-threaded single-process worker
if [ ! -z ${PGADMIN_ENABLE_TLS} ]; then
- exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} --keyfile /certs/server.key --certfile /certs/server.cert run_pgadmin:app
+ exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
else
- exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} run_pgadmin:app
+ exec gunicorn --timeout ${TIMEOUT} --bind ${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80} -w 1 --threads ${GUNICORN_THREADS:-25} --access-logfile ${GUNICORN_ACCESS_LOGFILE:--} -c gunicorn_config.py run_pgadmin:app
fi
diff --git a/pkg/docker/gunicorn_config.py b/pkg/docker/gunicorn_config.py
new file mode 100644
index 0000000..513c889
--- /dev/null
+++ b/pkg/docker/gunicorn_config.py
@@ -0,0 +1,2 @@
+import gunicorn
+gunicorn.SERVER_SOFTWARE = 'Python'
diff --git a/requirements.txt b/requirements.txt
index a5815a3..dbb0083 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -41,4 +41,5 @@ Flask-Security-Too>=3.0.0
bcrypt<=3.1.7
cryptography<=3.0
sshtunnel>=0.1.5
+netaddr==0.8.0
ldap3>=2.5.1
diff --git a/web/config.py b/web/config.py
index 702e73f..b893e35 100644
--- a/web/config.py
+++ b/web/config.py
@@ -143,12 +143,57 @@ DEFAULT_SERVER = '127.0.0.1'
# environment by the runtime
DEFAULT_SERVER_PORT = 5050
+# This param is used to validate ALLOWED_HOSTS for the application
+# This will be used to avoid Host Header Injection attack
+# For how to set ALLOWED_HOSTS see netaddr library
+# For more details https://netaddr.readthedocs.io/en/latest/tutorial_03.html
+# e.g. ALLOWED_HOSTS = ['192.0.2.0/28', '::192.0.2.0/124']
+# ALLOWED_HOSTS = ['225.0.0.0/8', '226.0.0.0/7', '228.0.0.0/6']
+# ALLOWED_HOSTS = ['127.0.0.1', '192.168.0.1']
+# if ALLOWED_HOSTS= [] then it will accept all ips (and application will be
+# vulnerable to Host Header Injection attack)
+ALLOWED_HOSTS = []
+
+# This param is used to override the default web server information about
+# the web technology and the frameworks being used in the application
+# An attacker could use this information to fingerprint underlying operating
+# system and research known exploits for the specific version of
+# software in use
+WEB_SERVER = 'Python'
+
# Enable X-Frame-Option protection.
# Set to one of "SAMEORIGIN", "ALLOW-FROM origin" or "" to disable.
# Note that "DENY" is NOT supported (and will be silently ignored).
# See https://tools.ietf.org/html/rfc7034 for more info.
X_FRAME_OPTIONS = "SAMEORIGIN"
+# The Content-Security-Policy header allows you to restrict how resources
+# such as JavaScript, CSS, or pretty much anything that the browser loads.
+# see https://content-security-policy.com/#source_list for more info
+# e.g. "default-src https: data: 'unsafe-inline' 'unsafe-eval';"
+CONTENT_SECURITY_POLICY = "default-src http: data: blob: 'unsafe-inline' " \
+ "'unsafe-eval';"
+
+# STRICT_TRANSPORT_SECURITY_ENABLED when set to True will set the
+# Strict-Transport-Security header
+STRICT_TRANSPORT_SECURITY_ENABLED = False
+
+# The Strict-Transport-Security header tells the browser to convert all HTTP
+# requests to HTTPS, preventing man-in-the-middle (MITM) attacks.
+# e.g. 'max-age=31536000; includeSubDomains'
+STRICT_TRANSPORT_SECURITY = "max-age=31536000; includeSubDomains"
+
+# The X-Content-Type-Options header forces the browser to honor the response
+# content type instead of trying to detect it, which can be abused to
+# generate a cross-site scripting (XSS) attack.
+# e.g. nosniff
+X_CONTENT_TYPE_OPTIONS = "nosniff"
+
+# The browser will try to prevent reflected XSS attacks by not loading the
+# page if the request contains something that looks like JavaScript and the
+# response contains the same data. e.g. '1; mode=block'
+X_XSS_PROTECTION = "1; mode=block"
+
# Hashing algorithm used for password storage
SECURITY_PASSWORD_HASH = 'pbkdf2_sha512'
@@ -421,12 +466,14 @@ ON_DEMAND_RECORD_COUNT = 1000
SHOW_GRAVATAR_IMAGE = True
##########################################################################
-# Set cookie path
+# Set cookie path and options
##########################################################################
COOKIE_DEFAULT_PATH = '/'
COOKIE_DEFAULT_DOMAIN = None
SESSION_COOKIE_DOMAIN = None
SESSION_COOKIE_SAMESITE = 'Lax'
+SESSION_COOKIE_SECURE = False
+SESSION_COOKIE_HTTPONLY = True
#########################################################################
# Skip storing session in files and cache for specific paths
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index 275f365..59cab62 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -12,6 +12,7 @@ such as setup of logging, dynamic loading of modules etc."""
import logging
import os
import sys
+import re
from types import MethodType
from collections import defaultdict
from importlib import import_module
@@ -19,11 +20,13 @@ from importlib import import_module
from flask import Flask, abort, request, current_app, session, url_for
from werkzeug.exceptions import HTTPException
from flask_babelex import Babel, gettext
+from flask_babelex import gettext as _
from flask_login import user_logged_in, user_logged_out
from flask_mail import Mail
from flask_paranoid import Paranoid
from flask_security import Security, SQLAlchemyUserDatastore, current_user
from flask_security.utils import login_user, logout_user
+from netaddr import IPSet
from werkzeug.datastructures import ImmutableDict
from werkzeug.local import LocalProxy
from werkzeug.utils import find_modules
@@ -36,9 +39,10 @@ from pgadmin.utils.session import create_session_interface, pga_unauthorised
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
from datetime import timedelta
from pgadmin.setup import get_version, set_version
-from pgadmin.utils.ajax import internal_server_error
+from pgadmin.utils.ajax import internal_server_error, make_json_response
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin import authenticate
+from pgadmin.utils.security_headers import SecurityHeaders
winreg = None
if os.name == 'nt':
@@ -658,6 +662,36 @@ def create_app(app_name=None):
request.endpoint not in ('security.login', 'security.logout'):
logout_user()
+ @app.before_request
+ def limit_host_addr():
+ """
+ This function validate the hosts from ALLOWED_HOSTS before allowing
+ HTTP request to avoid Host Header Injection attack
+ :return: None/JSON response with 403 HTTP status code
+ """
+ client_host = str(request.host).split(':')[0]
+ valid = True
+ allowed_hosts = config.ALLOWED_HOSTS
+
+ if len(allowed_hosts) != 0:
+ regex = re.compile(
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2}|)')
+ # Create separate list for ip addresses and host names
+ ip_set = list(filter(lambda ip: regex.match(ip), allowed_hosts))
+ host_set = list(filter(lambda ip: not regex.match(ip),
+ allowed_hosts))
+ is_ip = regex.match(client_host)
+ if is_ip:
+ valid = IPSet(ip_set).__contains__(client_host)
+ else:
+ valid = host_set.__contains__(client_host)
+
+ if not valid:
+ return make_json_response(
+ status=403, success=0,
+ errormsg=_("403 FORBIDDEN")
+ )
+
@app.after_request
def after_request(response):
if 'key' in request.args:
@@ -667,13 +701,12 @@ def create_app(app_name=None):
domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
response.set_cookie('PGADMIN_INT_KEY', value=request.args['key'],
path=config.COOKIE_DEFAULT_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
- # X-Frame-Options for security
- if config.X_FRAME_OPTIONS != "" and \
- config.X_FRAME_OPTIONS.lower() != "deny":
- response.headers["X-Frame-Options"] = config.X_FRAME_OPTIONS
-
+ SecurityHeaders.set_response_headers(response)
return response
##########################################################################
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 5375e94..6ed138b 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -697,6 +697,9 @@ def index():
response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
return response
diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py
index db11d1d..d05c1c8 100644
--- a/web/pgadmin/preferences/__init__.py
+++ b/web/pgadmin/preferences/__init__.py
@@ -232,6 +232,9 @@ def save(pid):
setattr(session, 'PGADMIN_LANGUAGE', language)
response.set_cookie("PGADMIN_LANGUAGE", value=language,
path=config.COOKIE_DEFAULT_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
**domain)
return response
diff --git a/web/pgadmin/utils/security_headers.py b/web/pgadmin/utils/security_headers.py
new file mode 100644
index 0000000..d85b5eb
--- /dev/null
+++ b/web/pgadmin/utils/security_headers.py
@@ -0,0 +1,41 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+#########################################################################
+
+import config
+
+
+class SecurityHeaders:
+
+ @staticmethod
+ def set_response_headers(response):
+ """set response security headers"""
+
+ params_dict = {
+ 'CONTENT_SECURITY_POLICY': 'Content-Security-Policy',
+ 'X_CONTENT_TYPE_OPTIONS': 'X-Content-Type-Options',
+ 'X_XSS_PROTECTION': 'X-XSS-Protection',
+ 'WEB_SERVER': 'Server',
+ }
+
+ # X-Frame-Options for security
+ if config.X_FRAME_OPTIONS != "" and \
+ config.X_FRAME_OPTIONS.lower() != "deny":
+ response.headers["X-Frame-Options"] = config.X_FRAME_OPTIONS
+
+ # Strict-Transport-Security
+ if config.STRICT_TRANSPORT_SECURITY_ENABLED and \
+ config.STRICT_TRANSPORT_SECURITY != "":
+ response.headers["Strict-Transport-Security"] = \
+ config.STRICT_TRANSPORT_SECURITY
+
+ # add other security options
+ for key in params_dict:
+ if key in config.__dict__ and config.__dict__[key] != "" \
+ and config.__dict__[key] is not None:
+ response.headers[params_dict[key]] = config.__dict__[key]
diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py
index b8af3d4..62e03ba 100644
--- a/web/pgadmin/utils/session.py
+++ b/web/pgadmin/utils/session.py
@@ -311,7 +311,11 @@ class ManagedSessionInterface(SessionInterface):
response.set_cookie(
app.session_cookie_name,
'%s!%s' % (session.sid, session.hmac_digest),
- expires=cookie_exp, httponly=True, domain=domain
+ expires=cookie_exp,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
+ domain=domain
)
^ permalink raw reply [nested|flat] 4+ messages in thread
* Re: [pgAdmin][5919] Fix security related issues
2020-10-19 12:01 [pgAdmin][5919] Fix security related issues Ganesh Jaybhay <[email protected]>
2020-10-19 12:08 ` Re: [pgAdmin][5919] Fix security related issues Dave Page <[email protected]>
2020-10-19 13:46 ` Re: [pgAdmin][5919] Fix security related issues Ganesh Jaybhay <[email protected]>
@ 2020-10-20 11:47 ` Akshay Joshi <[email protected]>
0 siblings, 0 replies; 4+ messages in thread
From: Akshay Joshi @ 2020-10-20 11:47 UTC (permalink / raw)
To: Ganesh Jaybhay <[email protected]>; +Cc: Dave Page <[email protected]>; pgadmin-hackers
Thanks, patch applied.
On Mon, Oct 19, 2020 at 7:17 PM Ganesh Jaybhay <
[email protected]> wrote:
> Thank you Dave for the suggestion.
>
> Please find the attached updated patch to make HSTS by default disabled
> and conditional based on flag.
>
> Regards,
> Ganesh Jaybhay
>
> On Mon, Oct 19, 2020 at 5:38 PM Dave Page <[email protected]> wrote:
>
>> Hi
>>
>> On Mon, Oct 19, 2020 at 1:01 PM Ganesh Jaybhay <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>>
>>> Please find the attached patch to fix the below security issues:
>>>
>>> - Host Header Injection - Added ALLOWED_HOSTS list to limit host
>>> address
>>> - Lack of Content Security Policy (CSP) - Added security header
>>> - Lack of Protection Mechanisms - HSTS - Added security header
>>> - Lack of Cookie Attribute – Secure : Kept as False as secure limits
>>> cookies to HTTPS traffic only.
>>> - Information Disclosure – Web Server / Development Framework
>>> VersionDescription: Kept as hard coded 'Python' instead of exposing
>>> wsgi/python/gunicorn version info.
>>>
>>> Please review and let me know if I have missed anything.
>>>
>>
>> I took a very quick look at this, and one thing that immediately stood
>> out is that HSTS should definitely not be enabled by default. That can make
>> dev/test/redeploy extremely difficult.
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EDB: http://www.enterprisedb.com
>>
>>
--
*Thanks & Regards*
*Akshay Joshi*
*pgAdmin Hacker | Sr. Software Architect*
*EDB Postgres <http://edbpostgres.com>*
*Mobile: +91 976-788-8246*
^ permalink raw reply [nested|flat] 4+ messages in thread
end of thread, other threads:[~2020-10-20 11:47 UTC | newest]
Thread overview: 4+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-10-19 12:01 [pgAdmin][5919] Fix security related issues Ganesh Jaybhay <[email protected]>
2020-10-19 12:08 ` Dave Page <[email protected]>
2020-10-19 13:46 ` Ganesh Jaybhay <[email protected]>
2020-10-20 11:47 ` Akshay Joshi <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox