Skip to content

Commit c858fdc

Browse files
authored
toil(front): restrict host in pollman (#454)
## 📝 Description restrict host in pollman ## ✅ Checklist - [x] I have tested this change - [x] ~This change requires documentation update~
1 parent a9ce84f commit c858fdc

File tree

2 files changed

+92
-22
lines changed

2 files changed

+92
-22
lines changed

front/assets/js/pollman.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,24 @@ export var Pollman = {
165165
},
166166

167167
fetchAndReplace: function (node, callback) {
168-
let url = Pollman.requestUrl(node);
168+
let url;
169+
170+
try {
171+
url = Pollman.requestUrl(node);
172+
} catch (error) {
173+
console.error('Invalid poll URL, removing element:', error);
174+
// Remove the invalid polling element to prevent further attempts
175+
$(node).remove();
176+
return;
177+
}
178+
169179
if (this.urlInProgress(url)) {
170180
return;
171181
}
172182

173183
let newFetch = this.rememberFetch(Date.now());
174-
175184
this.startForUrl(url);
185+
176186
fetch(url, { credentials: "same-origin" })
177187
.then(function (response) {
178188
if (response.status >= 200 && response.status < 300) {
@@ -230,15 +240,31 @@ export var Pollman = {
230240
},
231241

232242
requestUrl: function (node) {
233-
var queryParams = new URLSearchParams();
243+
const pollHref = node.getAttribute("data-poll-href");
244+
245+
try {
246+
const pollUrl = new URL(pollHref, window.location.origin);
234247

235-
Array.from(node.attributes).forEach(function (attribute) {
236-
if (attribute.name.startsWith("data-poll-param-")) {
237-
var name = attribute.name.substring("data-poll-param-".length);
238-
queryParams.append(name, attribute.value);
248+
// Only allow same origin (same protocol, host, and port)
249+
if (pollUrl.origin !== window.location.origin) {
250+
console.error(`Blocked data-poll-href to unauthorized host: ${pollUrl.origin}`);
251+
throw new Error(`data-poll-href must be same-origin. Got: ${pollUrl.origin}, Expected: ${window.location.origin}`);
239252
}
240-
});
241253

242-
return `${node.getAttribute("data-poll-href")}?${queryParams}`;
254+
// Build query parameters from data-poll-param-* attributes (existing logic)
255+
var queryParams = new URLSearchParams();
256+
Array.from(node.attributes).forEach(function (attribute) {
257+
if (attribute.name.startsWith("data-poll-param-")) {
258+
var name = attribute.name.substring("data-poll-param-".length);
259+
queryParams.append(name, attribute.value);
260+
}
261+
});
262+
263+
return `${pollUrl.href}?${queryParams}`;
264+
265+
} catch (error) {
266+
console.error('Invalid data-poll-href URL:', pollHref, error);
267+
throw new Error('Invalid or unauthorized data-poll-href URL');
268+
}
243269
},
244270
};

front/assets/js/pollman.spec.js

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
/**
22
* @prettier
33
*/
4-
54
import { expect } from "chai";
65
import { Pollman } from "./pollman";
76
import sinon from "sinon";
87

98
describe("Pollman", () => {
109
describe("#requestUrl", () => {
10+
beforeEach(() => {
11+
// Mock window.location.origin for the host validation
12+
global.window = {
13+
location: {
14+
origin: "https://example.com"
15+
}
16+
};
17+
global.URL = class URL {
18+
constructor(url, base) {
19+
if (url.startsWith('/')) {
20+
this.href = base + url;
21+
this.origin = base;
22+
} else {
23+
this.href = url;
24+
// Extract origin from full URL
25+
const match = url.match(/^(https?:\/\/[^\/]+)/);
26+
this.origin = match ? match[1] : base;
27+
}
28+
}
29+
};
30+
});
31+
1132
it("returns the URL from where to fetch the updated HTML node", () => {
1233
let node_mock = {
1334
attributes: [
@@ -23,33 +44,62 @@ describe("Pollman", () => {
2344
return attribute.value;
2445
},
2546
};
47+
expect(Pollman.requestUrl(node_mock)).to.equal(
48+
"https://example.com/resources/get?page=1&color=green&state=passed"
49+
);
50+
});
51+
52+
it("throws error for external domain", () => {
53+
let node_mock = {
54+
attributes: [
55+
{ name: "data-poll-href", value: "https://malicious.com/script.js" },
56+
],
57+
getAttribute: function (name) {
58+
let attribute = this.attributes.find(
59+
(attribute) => attribute.name === name
60+
);
61+
return attribute.value;
62+
},
63+
};
64+
expect(() => Pollman.requestUrl(node_mock)).to.throw(
65+
"Invalid or unauthorized data-poll-href URL"
66+
);
67+
});
2668

69+
it("allows same origin with full URL", () => {
70+
let node_mock = {
71+
attributes: [
72+
{ name: "data-poll-href", value: "https://example.com/api/poll" },
73+
{ name: "data-poll-param-id", value: "123" },
74+
],
75+
getAttribute: function (name) {
76+
let attribute = this.attributes.find(
77+
(attribute) => attribute.name === name
78+
);
79+
return attribute.value;
80+
},
81+
};
2782
expect(Pollman.requestUrl(node_mock)).to.equal(
28-
"/resources/get?page=1&color=green&state=passed"
83+
"https://example.com/api/poll?id=123"
2984
);
3085
});
3186
});
3287

3388
describe("#elementsToRefresh", () => {
3489
before(function () {
3590
Pollman.init({startLooper: false});
36-
3791
document.body.innerHTML = `
3892
<div data-poll-background="" data-poll-href="/resources/1" data-poll-state="poll"></div>
3993
<div data-poll-href="/resources/2" data-poll-state="poll"></div>
4094
`;
4195
});
42-
4396
afterEach(function () {
4497
if (Pollman.pageIsVisible && Pollman.pageIsVisible.restore)
4598
Pollman.pageIsVisible.restore();
4699
});
47-
48100
it("returns correct elements for visible page", () => {
49101
sinon.stub(Pollman, "pageIsVisible").returns(true);
50-
51102
let elements = Pollman.elementsToRefresh();
52-
53103
expect(elements.length).to.equal(2);
54104
expect(elements[0].outerHTML).to.equal(
55105
`<div data-poll-background="" data-poll-href="/resources/1" data-poll-state="poll"></div>`
@@ -58,10 +108,8 @@ describe("Pollman", () => {
58108
`<div data-poll-href="/resources/2" data-poll-state="poll"></div>`
59109
);
60110
});
61-
62111
it("returns correct elements for visible page when forced to", () => {
63112
sinon.stub(Pollman, "pageIsVisible").returns(true);
64-
65113
let elements = Pollman.elementsToRefresh({ forceRefresh: true });
66114
expect(elements.length).to.equal(2);
67115
expect(elements[0].outerHTML).to.equal(
@@ -71,20 +119,16 @@ describe("Pollman", () => {
71119
`<div data-poll-href="/resources/2" data-poll-state="poll"></div>`
72120
);
73121
});
74-
75122
it("returns correct elements for not active tab", () => {
76123
sinon.stub(Pollman, "pageIsVisible").returns(false);
77-
78124
let elements = Pollman.elementsToRefresh();
79125
expect(elements.length).to.equal(1);
80126
expect(elements[0].outerHTML).to.equal(
81127
`<div data-poll-background="" data-poll-href="/resources/1" data-poll-state="poll"></div>`
82128
);
83129
});
84-
85130
it("returns correct elements for not active tab when forced to ", () => {
86131
sinon.stub(Pollman, "pageIsVisible").returns(false);
87-
88132
let elements = Pollman.elementsToRefresh({ forceRefresh: true });
89133
expect(elements.length).to.equal(2);
90134
expect(elements[0].outerHTML).to.equal(

0 commit comments

Comments
 (0)