diff --git a/core/misc/active-link.js b/core/misc/active-link.js
index 464e9a4044f0af4b7391edccd9ec48a623bfbe45..acc8bf8daaabc8dd509283a74708a49d2eba5e6e 100644
--- a/core/misc/active-link.js
+++ b/core/misc/active-link.js
@@ -20,15 +20,42 @@
    */
   Drupal.behaviors.activeLinks = {
     attach(context) {
+      // JSON encode an object.
+      const jsonEncode = (object, stringify) => {
+        object = structuredClone(object);
+        const entries = Object.entries(object);
+
+        for (let i = 0; i < entries.length; i += 1) {
+          const [key, value] = entries[i];
+
+          if (typeof value === 'string') {
+            object[key] = value.replace(
+              /[\u007F-\uFFFF\u003c\u003e\u0022\u0027\u0026]/g,
+              (chr) => {
+                return '&u' + ('0000' + chr.charCodeAt(0).toString(16)).substr(-4);
+              }
+            );
+          }
+          else if (typeof value === 'object') {
+            object[key] = jsonEncode(value, false);
+          }
+        }
+
+        if (stringify === false) {
+          return object;
+        }
+
+        return JSON.stringify(object).replace('&u', '\\u');
+      };
+
       // Start by finding all potentially active links.
       const path = drupalSettings.path;
-      const queryString = JSON.stringify(path.currentQuery);
-      const querySelector = queryString
-        ? `[data-drupal-link-query="${CSS.escape(queryString)}"]`
-        : ':not([data-drupal-link-query])';
       const originalSelectors = [
         `[data-drupal-link-system-path="${CSS.escape(path.currentPath)}"]`,
       ];
+      const querySelector = path.currentQuery
+        ? `[data-drupal-link-query="${CSS.escape(jsonEncode(path.currentQuery))}"]`
+        : ':not([data-drupal-link-query])';
       let selectors;
 
       // If this is the front page, we have to check for the <front> path as