mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-22 00:37:36 -06:00
Added following security enhancements:
1) Added ALLOWED_HOSTS list to limit the host address. 2) Added CSP and HSTS security header. 3) Hide the webserver/ development framework version. Fixes #5919
This commit is contained in:
parent
3413a42af4
commit
08c4deba5a
@ -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
|
||||
|
@ -22,3 +22,4 @@ Bug fixes
|
||||
| `Issue #5858 <https://redmine.postgresql.org/issues/5858>`_ - Ensure that search object functionality works with case insensitive string.
|
||||
| `Issue #5895 <https://redmine.postgresql.org/issues/5895>`_ - Fixed an issue where the suffix for Toast table size is not visible in the Statistics tab.
|
||||
| `Issue #5911 <https://redmine.postgresql.org/issues/5911>`_ - Ensure that macros should be run on the older version of Safari and Chrome.
|
||||
| `Issue #5919 <https://redmine.postgresql.org/issues/5919>`_ - Added security related enhancements.
|
||||
|
@ -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
|
||||
|
2
pkg/docker/gunicorn_config.py
Normal file
2
pkg/docker/gunicorn_config.py
Normal file
@ -0,0 +1,2 @@
|
||||
import gunicorn
|
||||
gunicorn.SERVER_SOFTWARE = 'Python'
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
##########################################################################
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
41
web/pgadmin/utils/security_headers.py
Normal file
41
web/pgadmin/utils/security_headers.py
Normal file
@ -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]
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user