Security scan updates

This commit is contained in:
alexjanousekGSA
2025-07-31 12:47:40 -04:00
parent 55340dbc4b
commit 611ada696f
25 changed files with 114 additions and 152 deletions

View File

@@ -185,8 +185,7 @@ def _csp(config):
def create_app(application):
@application.after_request
def add_security_headers(response):
# Add Cross-Origin-Embedder-Policy header
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
response.headers['Cross-Origin-Embedder-Policy'] = 'credentialless'
return response
@application.context_processor
@@ -274,14 +273,11 @@ def create_app(application):
content_security_policy=_csp(application.config),
content_security_policy_nonce_in=["style-src", "script-src"],
permissions_policy={
"accelerometer": "()",
"ambient-light-sensor": "()",
"autoplay": "()",
"battery": "()",
"accelerometer": "(self \"https://www.youtube-nocookie.com\")",
"autoplay": "(self \"https://www.youtube-nocookie.com\")",
"camera": "()",
"document-domain": "()",
"geolocation": "()",
"gyroscope": "()",
"gyroscope": "(self \"https://www.youtube-nocookie.com\")",
"local-fonts": "()",
"magnetometer": "()",
"microphone": "()",

View File

@@ -25,30 +25,31 @@
this.bindEvents();
};
ListEntry.optionalAttributes = ['aria-describedby'];
ListEntry.prototype.entryTemplate = Hogan.compile(
'<div class="list-entry">' +
'<label for="{{{id}}}" class="govuk-input--numbered__label">' +
'<span class="usa-sr-only">{{listItemName}} number </span>{{number}}.' +
'</label>' +
'<input' +
' name="{{name}}"' +
' id="{{id}}"' +
' {{#value}}value="{{value}}{{/value}}"' +
' {{{sharedAttributes}}}' +
'/>' +
'{{#button}}' +
'<button type="button" class="usa-button input-list__button--remove">' +
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' +
'</button>' +
'{{/button}}' +
'</div>'
);
ListEntry.prototype.addButtonTemplate = Hogan.compile(
'<button type="button" class="usa-button input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
);
ListEntry.prototype.renderEntry = function(data) {
return `
<div class="list-entry">
<label for="${data.id}" class="govuk-input--numbered__label">
<span class="usa-sr-only">${data.listItemName} number </span>${data.number}.
</label>
<input
name="${data.name}"
id="${data.id}"
${data.value ? `value="${data.value}"` : ''}
${data.sharedAttributes}
/>
${data.button ? `
<button type="button" class="usa-button input-list__button--remove">
Remove<span class="usa-sr-only"> ${data.listItemName} number ${data.number}</span>
</button>
` : ''}
</div>
`;
};
ListEntry.prototype.renderAddButton = function(data) {
return `<button type="button" class="usa-button input-list__button--add">Add another ${data.listItemName} (${data.entriesLeft} remaining)</button>`;
};
ListEntry.prototype.getSharedAttributes = function () {
var $inputs = this.$wrapper.find('input'),
attributeTemplate = Hogan.compile(' {{name}}="{{value}}"'),
generatedAttributes = ['id', 'name', 'value'],
attributes = [],
attrIdx,
@@ -68,7 +69,7 @@
while (attrIdx--) {
// prevent duplicates
if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) {
attrStr += attributeTemplate.render({ 'name': elmAttrs[attrIdx].name, 'value': elmAttrs[attrIdx].value });
attrStr += ` ${elmAttrs[attrIdx].name}="${elmAttrs[attrIdx].value}"`;
existingAttributes.push(elmAttrs[attrIdx].name);
}
}
@@ -188,10 +189,10 @@
if (entryNumber > 1) {
dataObj.button = true;
}
this.$wrapper.append(this.entryTemplate.render(dataObj));
this.$wrapper.append(this.renderEntry(dataObj));
}.bind(this));
if (this.entries.length < this.maxEntries) {
this.$wrapper.append(this.addButtonTemplate.render({
this.$wrapper.append(this.renderAddButton({
'listItemName' : this.listItemName,
'entriesLeft' : (this.maxEntries - this.entries.length)
}));

View File

@@ -3,65 +3,70 @@
"use strict";
var Modules = global.GOVUK.Modules;
var Hogan = global.Hogan;
// Object holding all the states for the component's HTML
let states = {
'initial': Hogan.compile(`
{{#showNowAsDefault}}
// Template functions for rendering component states
let renderStates = {
'initial': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
</div>
${data.categories.map(category =>
`<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${category}' />`
).join('')}
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#categories}}
<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='{{.}}' />
{{/categories}}
</div>
`),
'choose': Hogan.compile(`
{{#showNowAsDefault}}
`;
},
'choose': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
</div>
${data.choices.map(choice => `
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#choices}}
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
`;
},
'chosen': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
{{/choices}}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div>
`),
'chosen': Hogan.compile(`
{{#showNowAsDefault}}
` : ''}
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
</div>
${data.choices.map(choice => `
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#choices}}
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
</div>
<div class="radio-select__column margin-y-2">
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
</div>
`)
<div class="radio-select__column margin-y-2">
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
</div>
`;
}
};
let shiftFocus = function(elementToFocus, component) {
@@ -80,7 +85,7 @@
let $component = $(component);
let render = (state, data) => {
$component.html(states[state].render(data));
$component.html(renderStates[state](data));
};
// store array of all options in component
let choices = $('label', $component).toArray().map(function(element) {

View File

@@ -25,7 +25,7 @@
<p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
can create an account and get started within minutes. We take the security and privacy of messaging data seriously
by minimizing data retention and using modern encryption methods.</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&amp;start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&amp;start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen credentialless></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
{% set product_highlights = [
{
"svg_src": "#send",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -44,7 +44,6 @@ const javascripts = () => {
.pipe(buffer())
.pipe(
plugins.addSrc.prepend([
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'textarea-caret/index.js',

49
package-lock.json generated
View File

@@ -18,7 +18,6 @@
"govuk_frontend_toolkit": "^9.0.1",
"govuk-frontend": "2.13.0",
"gulp-merge": "^0.1.1",
"hogan": "1.0.2",
"jquery": "3.7.1",
"morphdom": "^2.7.7",
"playwright": "^1.54.1",
@@ -4404,11 +4403,6 @@
"sass-embedded-linux-x64": "^1.89.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -8856,26 +8850,6 @@
"node": ">= 0.4"
}
},
"node_modules/hogan": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hogan/-/hogan-1.0.2.tgz",
"integrity": "sha512-2RV7G4f+Rt9YIYDu01r6pgZvP+XhrXi/JKlXd4b+vRybXk94ui4PQjbh/lFaH8gQtxCygy/WKkqmpm0IyZysJA==",
"dependencies": {
"hogan.js": "*"
}
},
"node_modules/hogan.js": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
"dependencies": {
"mkdirp": "0.3.0",
"nopt": "1.0.10"
},
"bin": {
"hulk": "bin/hulk"
}
},
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -11489,15 +11463,6 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true
},
"node_modules/mkdirp": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
"engines": {
"node": "*"
}
},
"node_modules/morphdom": {
"version": "2.7.7",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz",
@@ -11593,20 +11558,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "*"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@@ -34,7 +34,6 @@
"govuk_frontend_toolkit": "^9.0.1",
"govuk-frontend": "2.13.0",
"gulp-merge": "^0.1.1",
"hogan": "1.0.2",
"jquery": "3.7.1",
"morphdom": "^2.7.7",
"playwright": "^1.54.1",

View File

@@ -1,4 +1,3 @@
"""Simple tests to verify ZAP security fixes"""
import pytest
@@ -24,11 +23,23 @@ def test_no_duplicate_form_action(client_request, mocker, mock_get_service_and_o
assert count == 1
def test_cross_origin_embedder_policy_exists(client_request, mocker, mock_get_service_and_organization_counts):
"""Check that Cross-Origin-Embedder-Policy header is present"""
def test_cross_origin_embedder_policy_set_to_credentialless(client_request, mocker, mock_get_service_and_organization_counts):
"""Check that Cross-Origin-Embedder-Policy is set to 'credentialless' for YouTube compatibility"""
mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user")
client_request.logout()
response = client_request.get_response('.index')
assert 'Cross-Origin-Embedder-Policy' in response.headers
assert response.headers['Cross-Origin-Embedder-Policy'] == 'credentialless'
assert response.headers.get('Cross-Origin-Embedder-Policy') == 'credentialless'
def test_permissions_policy_allows_youtube_features(client_request, mocker, mock_get_service_and_organization_counts):
"""Check that Permissions-Policy allows necessary features for YouTube embeds"""
mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user")
client_request.logout()
response = client_request.get_response('.index')
permissions_policy = response.headers.get('Permissions-Policy', '')
assert 'accelerometer=(self "https://www.youtube-nocookie.com")' in permissions_policy
assert 'autoplay=(self "https://www.youtube-nocookie.com")' in permissions_policy
assert 'gyroscope=(self "https://www.youtube-nocookie.com")' in permissions_policy

20
urls.js
View File

@@ -19,17 +19,17 @@ const routes = {
authenticated: [
{
label: 'SMS Template Preview',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/send/a4e530e7-cae2-4e24-a1e9-752141a0b58e/one-off/step-0',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/send/9c2b7a55-8785-4dc6-84d6-eb0e0615590d/one-off/step-0',
},
// Pages with govuk buttons that need testing
{
label: 'API Keys',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/api/keys',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/api/keys',
},
// Pages to test radio buttons before converting govukRadios to USWDS
{
label: 'API Keys Create',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/api/keys/create',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/api/keys/create',
},
{
label: 'Change User Auth',
@@ -41,31 +41,31 @@ const routes = {
},
{
label: 'Service Settings',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings',
},
{
label: 'Service Send Files By Email',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings/send-files-by-email',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings/send-files-by-email',
},
{
label: 'Service SMS Prefix',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings/sms-prefix',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings/sms-prefix',
},
{
label: 'Send One Off Step 2',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/send/a4e530e7-cae2-4e24-a1e9-752141a0b58e/one-off/step-2',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/send/9c2b7a55-8785-4dc6-84d6-eb0e0615590d/one-off/step-2',
},
{
label: 'Choose Template',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/templates',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/templates',
},
{
label: 'Team Members',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/users',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users',
},
{
label: 'Invite User',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/users/invite',
path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users/invite',
},
// Platform admin pages with checkboxes
{