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
16 changes: 13 additions & 3 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ require('../css/app.css');
import bootstrap from 'bootstrap/dist/js/bootstrap.bundle';
import Mark from 'mark.js/src/vanilla';
import Autocomplete from './autocomplete';
import { toggleVisibilityClasses } from './helpers';
import { sanitizeUrl, toggleVisibilityClasses } from './helpers';

// Provide Bootstrap variable globally to allow custom backend pages to use it
window.bootstrap = bootstrap;
Expand Down Expand Up @@ -198,8 +198,15 @@ class App {

const filterModal = document.querySelector(filterButton.getAttribute('data-bs-target'));

// the filter URL is fetched and its response is injected into the page (see below),
// so it must be same-origin to prevent loading attacker-controlled remote HTML
const filtersUrl = sanitizeUrl(filterButton.getAttribute('data-href'), true);
if (null === filtersUrl) {
return;
}

// this is needed to avoid errors when connection is slow
filterButton.setAttribute('href', filterButton.getAttribute('data-href'));
filterButton.setAttribute('href', filtersUrl);
filterButton.removeAttribute('data-href');
filterButton.classList.remove('disabled');

Expand Down Expand Up @@ -665,7 +672,10 @@ class App {
return;
}
event.preventDefault();
window.location = element.getAttribute('data-ea-action-url');
const actionUrl = sanitizeUrl(element.getAttribute('data-ea-action-url'));
if (null !== actionUrl) {
window.location = actionUrl;
}
});
});
}
Expand Down
4 changes: 2 additions & 2 deletions assets/js/field-file-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class FileUploadField {
totalSizeInBytes += file.size;
}

this.#getFieldCustomInput().innerHTML = filename;
this.#getFieldCustomInput().textContent = filename;
this.#getFieldDeleteButton().style.display = 'block';
this.#getFieldSizeLabel().childNodes.forEach((fileUploadFileSizeLabelChild) => {
if (fileUploadFileSizeLabelChild.nodeType === Node.TEXT_NODE) {
Expand All @@ -57,7 +57,7 @@ class FileUploadField {
fieldDeleteCheckbox.click();
}
this.field.value = '';
this.#getFieldCustomInput().innerHTML = '';
this.#getFieldCustomInput().textContent = '';
toggleVisibilityClasses(this.#getFieldDeleteButton(), true);

this.#getFieldSizeLabel().childNodes.forEach((fileSizeLabelChild) => {
Expand Down
9 changes: 1 addition & 8 deletions assets/js/field-slug.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,8 @@ class Slugger {
this.lockButton.addEventListener('click', () => {
if (this.locked) {
const confirmMessage = this.field.dataset.confirmText || null;
if (null === confirmMessage) {
if (null === confirmMessage || true === confirm(confirmMessage)) {
this.unlock();
} else {
const formattedConfirmMessage = decodeURIComponent(
JSON.parse(`"${confirmMessage.replace(/\"/g, '\\"')}"`)
);
if (true === confirm(formattedConfirmMessage)) {
this.unlock();
}
}
} else {
this.lock();
Expand Down
29 changes: 29 additions & 0 deletions assets/js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,32 @@ export function toggleVisibilityClasses(element, removeVisibility) {
element.classList.add('d-block');
}
}

// Validates a URL read from the DOM before using it to navigate or fetch. It only
// allows safe schemes and rejects dangerous ones (e.g. "javascript:" or "data:") to
// prevent DOM-based XSS. When requireSameOrigin is true, cross-origin URLs are also
// rejected; this is required for flows that fetch a URL and inject the response into
// the page, so that attacker-controlled remote content can never be loaded. Returns
// the original URL when it's safe or null otherwise.
export function sanitizeUrl(url, requireSameOrigin = false) {
if (null === url || '' === url) {
return null;
}

try {
// relative URLs are resolved against the current origin; absolute URLs keep their own scheme
const parsedUrl = new URL(url, window.location.origin);
const allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
if (!allowedProtocols.includes(parsedUrl.protocol)) {
return null;
}

if (requireSameOrigin && parsedUrl.origin !== window.location.origin) {
return null;
}

return url;
} catch {
return null;
}
}
4 changes: 2 additions & 2 deletions public/app.70d109e6.js → public/app.33e2ad73.js

Large diffs are not rendered by default.

File renamed without changes.
6 changes: 3 additions & 3 deletions public/entrypoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"/app.9c5ece20.css"
],
"js": [
"/app.70d109e6.js"
"/app.33e2ad73.js"
]
},
"form": {
Expand Down Expand Up @@ -43,7 +43,7 @@
},
"field-file-upload": {
"js": [
"/field-file-upload.35a9e188.js"
"/field-file-upload.fc04f096.js"
]
},
"field-image": {
Expand All @@ -53,7 +53,7 @@
},
"field-slug": {
"js": [
"/field-slug.037a5045.js"
"/field-slug.bf0f4845.js"
]
},
"field-textarea": {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading