Merge pull request #2545 from alphagov/fix-csp-cdn

Fix content security policy for the CDN
This commit is contained in:
Chris Hill-Scott
2018-11-29 12:25:50 +00:00
committed by GitHub
9 changed files with 41 additions and 28 deletions

View File

@@ -70,7 +70,7 @@ from app.notify_client.complaint_api_client import complaint_api_client
from app.notify_client.platform_stats_api_client import platform_stats_api_client
from app.notify_client.template_folder_api_client import template_folder_api_client
from app.commands import setup_commands
from app.utils import get_cdn_domain, id_safe
from app.utils import get_logo_cdn_domain, id_safe
login_manager = LoginManager()
csrf = CSRFProtect()
@@ -500,13 +500,16 @@ def useful_headers_after_request(response):
response.headers.add('X-Content-Type-Options', 'nosniff')
response.headers.add('X-XSS-Protection', '1; mode=block')
response.headers.add('Content-Security-Policy', (
"default-src 'self' 'unsafe-inline';"
"script-src 'self' *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
"default-src 'self' {asset_domain} 'unsafe-inline';"
"script-src 'self' {asset_domain} *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
"connect-src 'self' *.google-analytics.com;"
"object-src 'self';"
"font-src 'self' data:;"
"img-src 'self' *.google-analytics.com *.notifications.service.gov.uk {} data:;"
"frame-src 'self' www.youtube.com;".format(get_cdn_domain())
"font-src 'self' {asset_domain} data:;"
"img-src 'self' {asset_domain} *.google-analytics.com *.notifications.service.gov.uk {logo_domain} data:;"
"frame-src 'self' www.youtube.com;".format(
asset_domain=current_app.config['ASSET_DOMAIN'],
logo_domain=get_logo_cdn_domain(),
)
))
if 'Cache-Control' in response.headers:
del response.headers['Cache-Control']

View File

@@ -74,6 +74,7 @@ class Config(object):
REDIS_URL = os.environ.get('REDIS_URL')
REDIS_ENABLED = os.environ.get('REDIS_ENABLED') == '1'
ASSET_DOMAIN = ''
ASSET_PATH = '/static/'
@@ -94,6 +95,8 @@ class Development(Config):
ANTIVIRUS_API_HOST = 'http://localhost:6016'
ANTIVIRUS_API_KEY = 'test-key'
ASSET_PATH = '/static/'
class Test(Development):
DEBUG = True
@@ -109,6 +112,8 @@ class Test(Development):
ANTIVIRUS_API_HOST = 'https://test-antivirus'
ANTIVIRUS_API_KEY = 'test-antivirus-secret'
ASSET_DOMAIN = 'static.example.com'
class Preview(Config):
HTTP_PROTOCOL = 'https'
@@ -119,6 +124,7 @@ class Preview(Config):
MOU_BUCKET_NAME = 'notify.works-mou'
NOTIFY_ENVIRONMENT = 'preview'
CHECK_PROXY_HEADER = False
ASSET_DOMAIN = 'static.notify.works'
ASSET_PATH = 'https://static.notify.works/'
@@ -132,6 +138,7 @@ class Staging(Config):
MOU_BUCKET_NAME = 'staging-notify.works-mou'
NOTIFY_ENVIRONMENT = 'staging'
CHECK_PROXY_HEADER = False
ASSET_DOMAIN = 'static.staging-notify.works'
ASSET_PATH = 'https://static.staging-notify.works/'
@@ -145,6 +152,7 @@ class Live(Config):
MOU_BUCKET_NAME = 'notifications.service.gov.uk-mou'
NOTIFY_ENVIRONMENT = 'live'
CHECK_PROXY_HEADER = False
ASSET_DOMAIN = 'static.notifications.service.gov.uk'
ASSET_PATH = 'https://static.notifications.service.gov.uk/'

View File

@@ -11,7 +11,7 @@ from app.main.s3_client import (
persist_logo,
upload_logo,
)
from app.utils import AgreementInfo, get_cdn_domain, user_is_platform_admin
from app.utils import AgreementInfo, get_logo_cdn_domain, user_is_platform_admin
@main.route("/email-branding", methods=['GET', 'POST'])
@@ -81,7 +81,7 @@ def update_email_branding(branding_id, logo=None):
'views/email-branding/manage-branding.html',
form=form,
email_branding=email_branding,
cdn_url=get_cdn_domain(),
cdn_url=get_logo_cdn_domain(),
logo=logo
)
@@ -126,7 +126,7 @@ def create_email_branding(logo=None):
return render_template(
'views/email-branding/manage-branding.html',
form=form,
cdn_url=get_cdn_domain(),
cdn_url=get_logo_cdn_domain(),
logo=logo
)

View File

@@ -16,7 +16,7 @@ from app import email_branding_client
from app.main import main
from app.main.forms import SearchTemplatesForm
from app.main.views.sub_navigation_dictionaries import features_nav
from app.utils import AgreementInfo, get_cdn_domain
from app.utils import AgreementInfo, get_logo_cdn_domain
@main.route('/')
@@ -108,7 +108,7 @@ def email_template():
colour = email_branding['colour']
brand_name = email_branding['text']
brand_colour = colour
brand_logo = ('https://{}/{}'.format(get_cdn_domain(), email_branding['logo'])
brand_logo = ('https://{}/{}'.format(get_logo_cdn_domain(), email_branding['logo'])
if email_branding['logo'] else None)
govuk_banner = branding_type in ['govuk', 'both']
brand_banner = branding_type == 'org_banner'

View File

@@ -54,7 +54,7 @@ from app.main.forms import (
from app.utils import (
AgreementInfo,
email_safe,
get_cdn_domain,
get_logo_cdn_domain,
user_has_permissions,
user_is_platform_admin,
)
@@ -991,7 +991,7 @@ def get_branding_as_value_and_label(email_branding):
def get_branding_as_dict(email_branding):
return {
branding['id']: {
'logo': 'https://{}/{}'.format(get_cdn_domain(), branding['logo']),
'logo': 'https://{}/{}'.format(get_logo_cdn_domain(), branding['logo']),
'colour': branding['colour']
} for branding in email_branding
}

View File

@@ -354,7 +354,7 @@ def email_or_sms_not_enabled(template_type, permissions):
return (template_type in ['email', 'sms']) and (template_type not in permissions)
def get_cdn_domain():
def get_logo_cdn_domain():
parsed_uri = urlparse(current_app.config['ADMIN_BASE_URL'])
if parsed_uri.netloc.startswith('localhost'):

View File

@@ -37,7 +37,7 @@ gulp.task('copy:govuk_template:css', () => gulp.src(paths.template + 'assets/sty
}))
.on('error', plugins.sass.logError)
.pipe(plugins.cssUrlAdjuster({
prependRelative: '/static/',
prependRelative: process.env.NOTIFY_ENVIRONMENT == 'development' ? '/static/' : '/',
}))
.pipe(gulp.dest(paths.dist + 'stylesheets/'))
);

View File

@@ -1,5 +1,5 @@
def test_owasp_useful_headers_set(client, mocker):
mocker.patch('app.get_cdn_domain', return_value='static-logos.test.com')
mocker.patch('app.get_logo_cdn_domain', return_value='static-logos.test.com')
response = client.get('/')
@@ -8,28 +8,30 @@ def test_owasp_useful_headers_set(client, mocker):
assert response.headers['X-Content-Type-Options'] == 'nosniff'
assert response.headers['X-XSS-Protection'] == '1; mode=block'
assert response.headers['Content-Security-Policy'] == (
"default-src 'self' 'unsafe-inline';"
"script-src 'self' *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
"default-src 'self' static.example.com 'unsafe-inline';"
"script-src 'self' static.example.com *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
"connect-src 'self' *.google-analytics.com;"
"object-src 'self';"
"font-src 'self' data:;"
"img-src 'self' *.google-analytics.com *.notifications.service.gov.uk static-logos.test.com data:;"
"font-src 'self' static.example.com data:;"
"img-src "
"'self' static.example.com *.google-analytics.com *.notifications.service.gov.uk static-logos.test.com data:;"
"frame-src 'self' www.youtube.com;"
)
def test_headers_non_ascii_characters_are_replaced(client, mocker):
mocker.patch('app.get_cdn_domain', return_value='static-logos۾.test.com')
mocker.patch('app.get_logo_cdn_domain', return_value='static-logos۾.test.com')
response = client.get('/')
assert response.status_code == 200
assert response.headers['Content-Security-Policy'] == (
"default-src 'self' 'unsafe-inline';"
"script-src 'self' *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
"default-src 'self' static.example.com 'unsafe-inline';"
"script-src 'self' static.example.com *.google-analytics.com 'unsafe-inline' 'unsafe-eval' data:;"
"connect-src 'self' *.google-analytics.com;"
"object-src 'self';"
"font-src 'self' data:;"
"img-src 'self' *.google-analytics.com *.notifications.service.gov.uk static-logos??.test.com data:;"
"font-src 'self' static.example.com data:;"
"img-src "
"'self' static.example.com *.google-analytics.com *.notifications.service.gov.uk static-logos??.test.com data:;"
"frame-src 'self' www.youtube.com;"
)

View File

@@ -16,7 +16,7 @@ from app.utils import (
generate_next_dict,
generate_notifications_csv,
generate_previous_dict,
get_cdn_domain,
get_logo_cdn_domain,
)
from tests.conftest import fake_uuid
@@ -278,13 +278,13 @@ def test_generate_notifications_csv_calls_twice_if_next_link(
def test_get_cdn_domain_on_localhost(client, mocker):
mocker.patch.dict('app.current_app.config', values={'ADMIN_BASE_URL': 'http://localhost:6012'})
domain = get_cdn_domain()
domain = get_logo_cdn_domain()
assert domain == 'static-logos.notify.tools'
def test_get_cdn_domain_on_non_localhost(client, mocker):
mocker.patch.dict('app.current_app.config', values={'ADMIN_BASE_URL': 'https://some.admintest.com'})
domain = get_cdn_domain()
domain = get_logo_cdn_domain()
assert domain == 'static-logos.admintest.com'