homebridge-roborock-vacuum2 1.4.13 → 1.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.14
4
+ - Hardened region detection by parsing the configured Roborock host instead of using substring matches.
5
+ - Sanitized map obstacle image URLs before assigning them in the browser UI to reduce XSS and client-side redirect risk.
6
+ - Added explicit read-only permissions to the CI workflow, upgraded GitHub Actions versions, and moved Codecov uploads to a repository secret.
7
+
3
8
  ## 1.4.13
4
9
  - Adjusted `package.json` repository metadata to match the fork URL exactly for npm Trusted Publishing compatibility.
5
10
  - Updated the npm publish workflow to use Node 24 and the latest npm CLI for Trusted Publishing compatibility.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-vacuum2",
3
- "version": "1.4.13",
3
+ "version": "1.4.14",
4
4
  "description": "Roborock Vacuum Cleaner - plugin for Homebridge.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -42,6 +42,44 @@ window.onload = function () {
42
42
  var largePhoto = document.getElementById("largePhoto");
43
43
  var largePhotoImage = document.getElementById("largePhoto-image");
44
44
 
45
+ function getSafeImageSource(rawValue) {
46
+ if (typeof rawValue !== "string") {
47
+ return null;
48
+ }
49
+
50
+ const value = rawValue.trim();
51
+ if (!value) {
52
+ return null;
53
+ }
54
+
55
+ if (/^data:image\//i.test(value) || value.startsWith("blob:")) {
56
+ return value;
57
+ }
58
+
59
+ try {
60
+ const imageURL = new URL(value, window.location.origin);
61
+ if (!["http:", "https:"].includes(imageURL.protocol)) {
62
+ return null;
63
+ }
64
+ if (imageURL.hostname !== window.location.hostname) {
65
+ return null;
66
+ }
67
+ return imageURL.toString();
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function applySafeImageSource(imageElement, rawValue) {
74
+ const safeImageSource = getSafeImageSource(rawValue);
75
+ if (!safeImageSource) {
76
+ console.warn("Ignoring unsafe obstacle image source");
77
+ return false;
78
+ }
79
+ imageElement.src = safeImageSource;
80
+ return true;
81
+ }
82
+
45
83
  var socket = new WebSocket("ws://" + window.location.hostname + ":7906");
46
84
 
47
85
  socket.onopen = () => {
@@ -292,8 +330,7 @@ window.onload = function () {
292
330
  socket.onmessage = function (event) {
293
331
  const serverData = JSON.parse(event.data);
294
332
 
295
- if (serverData.image) {
296
- popupImage.src = serverData.image;
333
+ if (serverData.image && applySafeImageSource(popupImage, serverData.image)) {
297
334
  socket.onmessage = null;
298
335
 
299
336
  popupImage.addEventListener("click", function () {
@@ -312,10 +349,9 @@ window.onload = function () {
312
349
  socket.onmessage = function (event) {
313
350
  const serverData = JSON.parse(event.data);
314
351
 
315
- if (serverData.image) {
352
+ if (serverData.image && applySafeImageSource(largePhotoImage, serverData.image)) {
316
353
  popup.style.display = "none";
317
354
  largePhoto.style.display = "block";
318
- largePhotoImage.src = serverData.image;
319
355
 
320
356
  largePhotoImage.onclick = function () {
321
357
  largePhoto.style.display = "none";
@@ -22,18 +22,27 @@ function normalizeBaseURL(baseURL) {
22
22
  return baseURL.replace(/^https?:\/\//i, "").replace(/\/+$/, "");
23
23
  }
24
24
 
25
+ function parseHostname(baseURL) {
26
+ try {
27
+ return new URL(`https://${normalizeBaseURL(baseURL)}`).hostname.toLowerCase();
28
+ } catch {
29
+ return normalizeBaseURL(baseURL).split("/")[0].split(":")[0].toLowerCase();
30
+ }
31
+ }
32
+
25
33
  function getRegionConfig(baseURL) {
26
- const lower = normalizeBaseURL(baseURL).toLowerCase();
27
- if (lower.includes("euiot")) {
34
+ const hostname = parseHostname(baseURL);
35
+ const labels = hostname.split(".").filter(Boolean);
36
+ if (labels.includes("euiot")) {
28
37
  return { country: "DE", countryCode: "49" };
29
38
  }
30
- if (lower.includes("usiot")) {
39
+ if (labels.includes("usiot")) {
31
40
  return { country: "US", countryCode: "1" };
32
41
  }
33
- if (lower.includes("cniot")) {
42
+ if (labels.includes("cniot")) {
34
43
  return { country: "CN", countryCode: "86" };
35
44
  }
36
- if (lower.includes("api.roborock.com")) {
45
+ if (hostname === "api.roborock.com") {
37
46
  return { country: "SG", countryCode: "65" };
38
47
  }
39
48
  return { country: "US", countryCode: "1" };