From fcd0604ca76d990266b59e26020f38ab907bed86 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 09:39:09 +0200 Subject: [PATCH 01/13] Unescape rendered errors in

---
 src/components/ErrorMessageDetails/index.jsx | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx
index c151245267..091e4bd11c 100644
--- a/src/components/ErrorMessageDetails/index.jsx
+++ b/src/components/ErrorMessageDetails/index.jsx
@@ -1,10 +1,17 @@
+function unescapeHTML(html) {
+  console.log(html);
+  const parser = new DOMParser();
+  const doc = parser.parseFromString(html, 'text/html');
+  return doc.documentElement.textContent.replace("'", "'");
+}
+
 export default function ErrorMessageDetails({ details }) {
   console.log(details);
   let children = [];
   if (details.messages) {
     if (details.kind) {
       children.push(
-        {details.kind && details.kind || "error"}
+        {(details.kind && details.kind != "plain") && details.kind || "error"}
         {details.base_branch && ` @ ${details.base_branch}` || null}
         {details.attempts && ` after ${details.attempts.toFixed(1)} attempts` || null}
       )
@@ -20,11 +27,11 @@ export default function ErrorMessageDetails({ details }) {
       )
     }
     for (const message of details.messages) {
-      children.push(
{message}
) + children.push(
{unescapeHTML(message)}
) } /* Legacy: string only values */ } else { - children.push(
{details.toString()}
) + children.push(
{unescapeHTML(details.toString())}
) } return <>{children} } From 059f4f99904bebe9d0e255d4f94400e30604337e Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 10:16:44 +0200 Subject: [PATCH 02/13] Same line for table badges --- src/components/ErrorMessageDetails/index.jsx | 34 +++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 091e4bd11c..34812c62f4 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -9,26 +9,30 @@ export default function ErrorMessageDetails({ details }) { console.log(details); let children = []; if (details.messages) { - if (details.kind) { - children.push( + if (details.kind || details.url) { + let header = [] + if (details.kind) { + header.push( {(details.kind && details.kind != "plain") && details.kind || "error"} {details.base_branch && ` @ ${details.base_branch}` || null} {details.attempts && ` after ${details.attempts.toFixed(1)} attempts` || null} ) + } + if (details.url) { + header.push( + + View CI job{" "} + + + + + ) + } + children.push(
{header}
) } - if (details.url) { - children.push( - - View CI job{" "} - - - - - ) - } - for (const message of details.messages) { - children.push(
{unescapeHTML(message)}
) - } + details.messages.map((message, index) => ( + children.push(
{unescapeHTML(message)}
) + )) /* Legacy: string only values */ } else { children.push(
{unescapeHTML(details.toString())}
) From ba6694ebba692845f64e1fab7651d4e08433bb6f Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 10:17:08 +0200 Subject: [PATCH 03/13] fix class warning (should be className) --- src/pages/status/migration/index.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/status/migration/index.jsx b/src/pages/status/migration/index.jsx index 31187e795b..41eb557e09 100644 --- a/src/pages/status/migration/index.jsx +++ b/src/pages/status/migration/index.jsx @@ -159,12 +159,12 @@ export default function MigrationDetails() {
-
-
    +
    +
      @@ -534,7 +534,7 @@ function Table({ details }) { {rows.map((row, i) => - {{ feedstock: feedstock[row.name], name: row.name, status: row.status }} + {{ feedstock: feedstock[row.name], name: row.name, status: row.status }} )} } From bca068a5c6202ce81127de29f7d70a18913d81e8 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 10:18:44 +0200 Subject: [PATCH 04/13] formatting --- src/components/ErrorMessageDetails/index.jsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 34812c62f4..8a455cb7e7 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -12,11 +12,13 @@ export default function ErrorMessageDetails({ details }) { if (details.kind || details.url) { let header = [] if (details.kind) { - header.push( - {(details.kind && details.kind != "plain") && details.kind || "error"} - {details.base_branch && ` @ ${details.base_branch}` || null} - {details.attempts && ` after ${details.attempts.toFixed(1)} attempts` || null} - ) + header.push( + + {(details.kind && details.kind != "plain") && details.kind || "error"} + {details.base_branch && ` @ ${details.base_branch}` || null} + {details.attempts && ` after ${details.attempts.toFixed(1)} attempts` || null} + + ) } if (details.url) { header.push( From 158dfc6859bee8f7275d0bfe0d43cbd6628696a2 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 14:33:07 +0200 Subject: [PATCH 05/13] add docstring --- src/components/ErrorMessageDetails/index.jsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 8a455cb7e7..7e2c72eeb3 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -1,12 +1,11 @@ -function unescapeHTML(html) { - console.log(html); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - return doc.documentElement.textContent.replace("'", "'"); -} +/* + This component deals with untrusted inputs. These details may come from externally-controlled + data (tracebacks in bot logic and other types of unhandled errors, branch names, etc). We + need to be careful not to render those inputs as arbitrary elements! + Thankfully, React escapes children strings before rendering, so that should take care of it. +*/ export default function ErrorMessageDetails({ details }) { - console.log(details); let children = []; if (details.messages) { if (details.kind || details.url) { From bafafb2d56df90d991b9ee34e05ce077f2f71fd0 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 14:33:20 +0200 Subject: [PATCH 06/13] use ternary op --- src/components/ErrorMessageDetails/index.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 7e2c72eeb3..2f848c0501 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -13,9 +13,9 @@ export default function ErrorMessageDetails({ details }) { if (details.kind) { header.push( - {(details.kind && details.kind != "plain") && details.kind || "error"} - {details.base_branch && ` @ ${details.base_branch}` || null} - {details.attempts && ` after ${details.attempts.toFixed(1)} attempts` || null} + {(details.kind && details.kind != "plain") ? details.kind : "error"} + {details.base_branch ? ` @ ${details.base_branch}` : null} + {details.attempts ? ` after ${details.attempts.toFixed(1)} attempts` : null} ) } From 7d5f14eeb74a843ee0f7b4af60a4b4f9123efc26 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 14:38:36 +0200 Subject: [PATCH 07/13] always pass string --- src/components/ErrorMessageDetails/index.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 2f848c0501..0188146e47 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -1,3 +1,9 @@ +function unescapeHTML(html) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + return doc.documentElement.textContent.replace("'", "'"); +} + /* This component deals with untrusted inputs. These details may come from externally-controlled data (tracebacks in bot logic and other types of unhandled errors, branch names, etc). We @@ -32,7 +38,7 @@ export default function ErrorMessageDetails({ details }) { children.push(
      {header}
      ) } details.messages.map((message, index) => ( - children.push(
      {unescapeHTML(message)}
      ) + children.push(
      {unescapeHTML(message.toString())}
      ) )) /* Legacy: string only values */ } else { From 60e55da9f3180397cfce8a80a74e2b6ddc6f3ae7 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 14:43:26 +0200 Subject: [PATCH 08/13] add two comments --- src/components/ErrorMessageDetails/index.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 0188146e47..5aa338751b 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -13,6 +13,7 @@ function unescapeHTML(html) { */ export default function ErrorMessageDetails({ details }) { let children = []; + /* Structured error metadata was introduced in 2026-06... */ if (details.messages) { if (details.kind || details.url) { let header = [] @@ -40,7 +41,7 @@ export default function ErrorMessageDetails({ details }) { details.messages.map((message, index) => ( children.push(
      {unescapeHTML(message.toString())}
      ) )) - /* Legacy: string only values */ + /* ... but bot data may still contain legacy, string only values */ } else { children.push(
      {unescapeHTML(details.toString())}
      ) } From c5636b316c53c638982b3f249816b6f4ef175216 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 15:00:51 +0200 Subject: [PATCH 09/13] sanitize URLs too --- src/components/ErrorMessageDetails/index.jsx | 21 +++++++++++++++++-- .../StatusDashboard/version_updates.jsx | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 5aa338751b..cce74a169d 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -4,6 +4,17 @@ function unescapeHTML(html) { return doc.documentElement.textContent.replace("'", "'"); } +function isSafeUrl(urlString) { + try { + const parsed = new URL(urlString); + // Explicitly allow only http: and https: protocols + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch (e) { + // If the URL constructor throws an error, the URL is invalid + return false; + } +}; + /* This component deals with untrusted inputs. These details may come from externally-controlled data (tracebacks in bot logic and other types of unhandled errors, branch names, etc). We @@ -26,9 +37,15 @@ export default function ErrorMessageDetails({ details }) { ) } - if (details.url) { + if (details.url && isSafeUrl(details.url)) { header.push( - + View CI job{" "} diff --git a/src/components/StatusDashboard/version_updates.jsx b/src/components/StatusDashboard/version_updates.jsx index 90bf455df5..0773080242 100644 --- a/src/components/StatusDashboard/version_updates.jsx +++ b/src/components/StatusDashboard/version_updates.jsx @@ -40,7 +40,7 @@ export default function VersionUpdates({ onLoad }) { {Object.keys(errors).length} {" "} - + From 1ad208119e2f7c94b4321672e1637050151d8579 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 15:02:38 +0200 Subject: [PATCH 10/13] replaceAll single quotes --- src/components/ErrorMessageDetails/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index cce74a169d..444beb8080 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -1,7 +1,7 @@ function unescapeHTML(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); - return doc.documentElement.textContent.replace("'", "'"); + return doc.documentElement.textContent.replaceAll("'", "'"); } function isSafeUrl(urlString) { From 9a98f4b2ad0e537e6d4db1d2529492806e04c8a3 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 15:02:49 +0200 Subject: [PATCH 11/13] prek --- src/components/ErrorMessageDetails/index.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index 444beb8080..e981b3b145 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -39,11 +39,11 @@ export default function ErrorMessageDetails({ details }) { } if (details.url && isSafeUrl(details.url)) { header.push( - View CI job{" "} From 3409881eebede091289c6019f8697b20d0a4953d Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 15:14:06 +0200 Subject: [PATCH 12/13] some more sanitized URLs --- src/components/ErrorMessageDetails/index.jsx | 17 +---------------- .../StatusDashboard/cloud_services.jsx | 10 +++++++--- .../StatusDashboard/repos_and_bots.jsx | 12 ++++++++---- src/pages/status/migration/index.jsx | 3 ++- src/utils.js | 16 ++++++++++++++++ 5 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 src/utils.js diff --git a/src/components/ErrorMessageDetails/index.jsx b/src/components/ErrorMessageDetails/index.jsx index e981b3b145..3b90fa490b 100644 --- a/src/components/ErrorMessageDetails/index.jsx +++ b/src/components/ErrorMessageDetails/index.jsx @@ -1,19 +1,4 @@ -function unescapeHTML(html) { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - return doc.documentElement.textContent.replaceAll("'", "'"); -} - -function isSafeUrl(urlString) { - try { - const parsed = new URL(urlString); - // Explicitly allow only http: and https: protocols - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; - } catch (e) { - // If the URL constructor throws an error, the URL is invalid - return false; - } -}; +import {isSafeUrl, unescapeHTML} from "@site/src/utils"; /* This component deals with untrusted inputs. These details may come from externally-controlled diff --git a/src/components/StatusDashboard/cloud_services.jsx b/src/components/StatusDashboard/cloud_services.jsx index 1aa37762f7..ce197c97ca 100644 --- a/src/components/StatusDashboard/cloud_services.jsx +++ b/src/components/StatusDashboard/cloud_services.jsx @@ -1,6 +1,7 @@ import { urls } from "@site/src/constants"; import { React, useEffect, useState } from "react"; import styles from "./styles.module.css"; +import { isSafeUrl } from "@site/src/utils"; const OPERATIONAL = "All Systems Operational"; @@ -53,9 +54,12 @@ function Status({ api, link, title }) { return ( - - {title} - + {link && isSafeUrl(link) ? + + {title} + + : title + } - - {children} - + {link && isSafeUrl(link) ? + + {children} + + : children + } {`${children}{badge} @@ -53,7 +57,7 @@ function Image({ alt, link, children }) { {alt} ); - return link ? {image} : image; + return link && isSafeUrl(link) ? {image} : image; } function CDNStatus() { diff --git a/src/pages/status/migration/index.jsx b/src/pages/status/migration/index.jsx index 41eb557e09..a2f360b79d 100644 --- a/src/pages/status/migration/index.jsx +++ b/src/pages/status/migration/index.jsx @@ -15,6 +15,7 @@ import { useSorting, SortableHeader } from '@site/src/components/SortableTable'; import * as d3 from "d3"; import DependencyGraph from "@site/src/components/DependencyGraph"; import ErrorMessageDetails from "@site/src/components/ErrorMessageDetails"; +import { isSafeUrl } from "@site/src/utils"; // GitHub GraphQL MergeStateStatus documentation // Reference: https://docs.github.com/en/graphql/reference/enums#mergestatestatus @@ -555,7 +556,7 @@ function Row({ children }) { return (<> - {href ? ( + {href && isSafeUrl(href) ? ( {name} ) : ( details ? ( diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000000..afb27141f0 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,16 @@ +export function isSafeUrl(urlString) { + try { + const parsed = new URL(urlString); + // Explicitly allow only http: and https: protocols + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch (e) { + // If the URL constructor throws an error, the URL is invalid + return false; + } +}; + +export function unescapeHTML(html) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + return doc.documentElement.textContent.replaceAll("'", "'"); +} From e866276edb6d02d30502efc49ffbe9f3e7238d99 Mon Sep 17 00:00:00 2001 From: jaimergp Date: Thu, 11 Jun 2026 15:14:32 +0200 Subject: [PATCH 13/13] prek --- src/components/StatusDashboard/repos_and_bots.jsx | 2 +- src/utils.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/StatusDashboard/repos_and_bots.jsx b/src/components/StatusDashboard/repos_and_bots.jsx index bc5d9908f3..90a33de8b8 100644 --- a/src/components/StatusDashboard/repos_and_bots.jsx +++ b/src/components/StatusDashboard/repos_and_bots.jsx @@ -35,7 +35,7 @@ function Badge({ children, link, badge, badgeLink }) { return ( - {link && isSafeUrl(link) ? + {link && isSafeUrl(link) ? {children} diff --git a/src/utils.js b/src/utils.js index afb27141f0..3ad248ab28 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,15 +2,15 @@ export function isSafeUrl(urlString) { try { const parsed = new URL(urlString); // Explicitly allow only http: and https: protocols - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch (e) { // If the URL constructor throws an error, the URL is invalid return false; } -}; +} export function unescapeHTML(html) { const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); + const doc = parser.parseFromString(html, "text/html"); return doc.documentElement.textContent.replaceAll("'", "'"); }