Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions web/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,29 @@
# 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 ws: http: data: blob: 'unsafe-inline'" \
" 'unsafe-eval';"
#
# A per-request nonce is used in place of 'unsafe-inline' for scripts. The
# literal token {nonce} anywhere in the policy is replaced at runtime with a
# freshly generated nonce that is also emitted on pgAdmin's inline <script>
# tags. To go back to the old permissive policy, set:
# CONTENT_SECURITY_POLICY = "default-src ws: http: data: blob:" \
# " 'unsafe-inline' 'unsafe-eval';"
# Notes:
# - 'unsafe-inline' is retained for style-src because pgAdmin's UI (React/MUI)
# injects styles at runtime that are not nonce tagged.
# - 'unsafe-eval' is NOT listed: production bundles do not need it.
# Development webpack bundles are built with the 'eval' devtool and DO
# need it, so when running the dev server add it in config_local.py, e.g.:
# CONTENT_SECURITY_POLICY = (
# "default-src 'self' ws: http: data: blob:;"
# " script-src 'self' 'nonce-{nonce}' 'unsafe-eval';"
# " style-src 'self' 'unsafe-inline';"
# )
CONTENT_SECURITY_POLICY = (
"default-src 'self' ws: http: data: blob:;"
" script-src 'self' 'nonce-{nonce}';"
" style-src 'self' 'unsafe-inline';"
)

# STRICT_TRANSPORT_SECURITY_ENABLED when set to True will set the
# Strict-Transport-Security header
Expand Down
3 changes: 3 additions & 0 deletions web/pgadmin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,9 @@ def inject_blueprint():
return {
'current_app': current_app,
'current_blueprint': current_blueprint,
# Per-request Content-Security-Policy nonce, for use on inline
# <script>/<style> tags when a nonce based CSP is configured.
'csp_nonce': SecurityHeaders.get_nonce(),
}

@app.errorhandler(Exception)
Expand Down
26 changes: 15 additions & 11 deletions web/pgadmin/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<!-- To set pgAdmin4 shortcut icon in browser -->
<link rel="shortcut icon" href="{{ url_for('redirects.favicon') }}"/>
<style>
<style nonce="{{ csp_nonce }}">
.pg-sp-container {
position: absolute;
min-width: 100%;
Expand Down Expand Up @@ -43,14 +43,18 @@
<!--View specified stylesheets-->
{% block css_link %}{% endblock %}

<script type="application/javascript">
<!-- Per-request CSP nonce for webpack's dynamically injected assets -->
<script type="application/javascript" nonce="{{ csp_nonce }}">
window.__webpack_nonce__ = "{{ csp_nonce }}";
</script>
<script type="application/javascript" nonce="{{ csp_nonce }}">
/* This is used to change publicPath of webpack at runtime */
window.resourceBasePath = "{{ url_for('static', filename='js') }}/generated/";
</script>
<script type="application/javascript"
<script type="application/javascript" nonce="{{ csp_nonce }}"
src="{{ url_for('static', filename='vendor/require/require.js' if config.DEBUG else 'vendor/require/require.min.js') }}"></script>
<!-- Base template scripts -->
<script type="application/javascript">
<script type="application/javascript" nonce="{{ csp_nonce }}">
require.config({
baseUrl: '',
{% if config.APP_VERSION_PARAM is not none and config.APP_VERSION_PARAM != '' %}
Expand All @@ -73,15 +77,15 @@
});

</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.react.js') }}" ></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.others.js') }}" ></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/vendor.sqleditor.js') }}"></script>
<script type="application/javascript" src="{{ url_for('static', filename='js/generated/pgadmin_commons.js') }}" ></script>
<script type="application/javascript" nonce="{{ csp_nonce }}" src="{{ url_for('static', filename='js/generated/vendor.react.js') }}" ></script>
<script type="application/javascript" nonce="{{ csp_nonce }}" src="{{ url_for('static', filename='js/generated/vendor.main.js') }}" ></script>
<script type="application/javascript" nonce="{{ csp_nonce }}" src="{{ url_for('static', filename='js/generated/vendor.others.js') }}" ></script>
<script type="application/javascript" nonce="{{ csp_nonce }}" src="{{ url_for('static', filename='js/generated/vendor.sqleditor.js') }}"></script>
<script type="application/javascript" nonce="{{ csp_nonce }}" src="{{ url_for('static', filename='js/generated/pgadmin_commons.js') }}" ></script>

</head>
<body>
<style>
<style nonce="{{ csp_nonce }}">
body {
-webkit-user-select: none;
-moz-user-select: none;
Expand All @@ -100,7 +104,7 @@
<![endif]-->

{% block body %}{% endblock %}
<script type="application/javascript">
<script type="application/javascript" nonce="{{ csp_nonce }}">
{% block init_script %}{% endblock %}
</script>

Expand Down
2 changes: 1 addition & 1 deletion web/pgadmin/templates/security/render_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
'messages': get_flashed_messages(with_categories=true)
} %}
{% block body %}
<style>
<style nonce="{{ csp_nonce }}">
#root:not(:empty) + .pg-sp-container {
display: none;
}
Expand Down
4 changes: 2 additions & 2 deletions web/pgadmin/tools/debugger/templates/debugger/direct.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}
{% endblock %}
{% block body %}
<style>
<style nonce="{{ csp_nonce }}">
#debugger-main-container {
display: flex;
flex-direction: column;
Expand All @@ -29,7 +29,7 @@
}
</style>
{% if is_desktop_mode and is_linux %}
<style>
<style nonce="{{ csp_nonce }}">
body
</style>
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion web/pgadmin/tools/erd/templates/erd/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
{% block body %}
<style>
<style nonce="{{ csp_nonce }}">
body {padding: 0px;}
#erd-tool-container {
display: flex;
Expand Down
2 changes: 1 addition & 1 deletion web/pgadmin/tools/psql/templates/psql/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% block title %}{{title}}{% endblock %}

{% block css_link %}
<style>
<style nonce="{{ csp_nonce }}">
#psql-container {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
}
{% endblock %}
{% block css_link %}
<style>
<style nonce="{{ csp_nonce }}">
#schema-diff-main-container {
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion web/pgadmin/tools/sqleditor/templates/sqleditor/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
{% block body %}
<style>
<style nonce="{{ csp_nonce }}">
body {padding: 0px;}
#sqleditor-container {
display: flex;
Expand Down
42 changes: 41 additions & 1 deletion web/pgadmin/utils/security_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,52 @@
#
#########################################################################

import secrets

from flask import g

import config

# Token that may be placed in the CONTENT_SECURITY_POLICY config value. It is
# substituted at runtime with a freshly generated, per-request nonce so that
# admins can opt-in to a nonce based policy, e.g.:
# CONTENT_SECURITY_POLICY = "script-src 'self' 'nonce-{nonce}';"
CSP_NONCE_PLACEHOLDER = '{nonce}'


class SecurityHeaders:

@staticmethod
def get_nonce():
"""
Return the Content-Security-Policy nonce for the current request.

The value is generated once and cached on flask.g so that the exact
same nonce is emitted both in the rendered template (on inline
<script>/<style> tags) and in the Content-Security-Policy response
header.
"""
if not hasattr(g, 'csp_nonce'):
g.csp_nonce = secrets.token_urlsafe(16)
return g.csp_nonce

@staticmethod
def get_content_security_policy():
"""
Return the configured Content-Security-Policy, replacing the
``{nonce}`` placeholder (if present) with the per-request nonce.
"""
csp = getattr(config, 'CONTENT_SECURITY_POLICY', None)
if csp and CSP_NONCE_PLACEHOLDER in csp:
csp = csp.replace(
CSP_NONCE_PLACEHOLDER, SecurityHeaders.get_nonce())
return csp

@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',
Expand All @@ -35,6 +70,11 @@ def set_response_headers(response):
response.headers["Strict-Transport-Security"] = \
config.STRICT_TRANSPORT_SECURITY

# Content-Security-Policy (with optional per-request nonce support)
csp = SecurityHeaders.get_content_security_policy()
if csp:
response.headers['Content-Security-Policy'] = csp

# add other security options
for key in params_dict:
if key in config.__dict__ and config.__dict__[key] != "" \
Expand Down
Loading