CVE-2024-32986: Insufficient Information (NVD-CWE-noinfo)
value: PWAsForFirefox is a tool to install, manage and use Progressive Web Apps (PWAs) in Mozilla Firefox. Due to improper sanitization of web app properties (such as name, description, shortcuts), web apps were able to inject additional lines into XDG Desktop Entries (on Linux) and `AppInfo.ini` (on PortableApps.com). This allowed malicious web apps to introduce keys like `Exec`, which could run arbitrary code when the affected web app was launched. This vulnerability affects all Linux and PortableApps.com users of all PWAsForFirefox versions up to (excluding) 2.12.0. Windows and macOS users are not affected. This vulnerability has been fixed in commit `9932d4b` which has been included in release in v2.12.0. The main fix is implemented in the native part, but the extension also contains additional fixes. All Linux and PortableApps.com users are advised to update to this version as soon as possible. It is also recommended for Windows and macOS users to update to this version, as it contains additional fixes related to properties sanitization. There are no known workarounds for this vulnerability.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-03T10:15Z
Repository: https://github.com/filips123/PWAsForFirefox
Language: Rust
Files changed: extension/src/sites/install.js, extension/src/sites/launch.js, extension/src/sites/manage.js, extension/src/utils.js, native/src/components/site.rs, native/src/connector/process.rs, native/src/console/profile.rs, native/src/console/site.rs, native/src/integrations/implementation/linux.rs, native/src/integrations/implementation/macos.rs, native/src/integrations/implementation/portableapps.rs, native/src/integrations/implementation/windows.rs, native/src/integrations/utils.rs, native/src/utils.rs, native/userchrome/profile/chrome/pwa/content/browser.jsm, native/userchrome/profile/chrome/pwa/content/macosHiddenWindow.jsm, native/userchrome/profile/chrome/pwa/utils/common.jsm, native/userchrome/profile/chrome/pwa/utils/systemIntegration.jsm
Commit date: 2024-04-30 18:24:45 +0200
Commit message: Fix improper sanitization of web app properties that lead to security vulnerabilities

This contains a critical security fix. More details will be published soon.

Diff: diff --git a/extension/src/sites/install.js b/extension/src/sites/install.js
index a271fca..1992fad 100644
--- a/extension/src/sites/install.js
+++ b/extension/src/sites/install.js
@@ -12,10 +12,11 @@ import {
   obtainManifest,
   obtainProfileList,
   obtainSiteList,
   obtainUrls,
   PREF_DEFAULT_PROFILE_TEMPLATE,
+  sanitizeString,
   setPopupSize
 } from '../utils'
 import { getMessage } from '../utils/i18n'
 import { knownCategories } from './categories'
 
@@ -134,14 +135,16 @@ async function initializeForm () {
   document.getElementById('web-app-name').setAttribute('placeholder', name)
   document.getElementById('web-app-description').setAttribute('placeholder', description)
   document.getElementById('web-app-start-url').setAttribute('placeholder', manifest?.start_url || documentUrl)
 
   const categoriesElement = document.getElementById('web-app-categories')
-  for (const category of manifest?.categories || []) categoriesElement.tagsInstance.addItem(category, category)
+  const categoriesList = manifest?.categories?.map(item => sanitizeString(item)).filter(item => item) || []
+  for (const category of categoriesList) categoriesElement.tagsInstance.addItem(category, category)
 
   const keywordsElement = document.getElementById('web-app-keywords')
-  for (const keyword of manifest?.keywords || []) keywordsElement.tagsInstance.addItem(keyword, keyword)
+  const keywordsList = manifest?.keywords?.map(item => sanitizeString(item)).filter(item => item) || []
+  for (const keyword of keywordsList) keywordsElement.tagsInstance.addItem(keyword, keyword)
 
   // Add available profiles to the select input
   const profilesElement = document.getElementById('web-app-profile')
   for (const profile of Object.values(profiles)) profilesElement.add(new Option(profile.name || profile.ulid, profile.ulid))
 
@@ -348,15 +351,15 @@ async function initializeForm () {
     const description = document.getElementById('web-app-description').value || null
 
     // Get categories and keywords based on user form input and site manifest
     // If the user list is identical to the manifest, ignore it, otherwise, set it as a user overwrite
     const userCategories = [...document.getElementById('web-app-categories').selectedOptions].map(option => option.value)
-    const manifestCategories = manifest?.categories || []
+    const manifestCategories = manifest?.categories?.map(item => sanitizeString(item)).filter(item => item) || []
     const categories = userCategories.toString() !== manifestCategories.toString() ? userCategories : null
 
     const userKeywords = [...document.getElementById('web-app-keywords').selectedOptions].map(option => option.value)
-    const manifestKeywords = manifest?.keywords || []
+    const manifestKeywords = manifest?.keywords?.map(item => sanitizeString(item)).filter(item => item) || []
     const keywords = userKeywords.toString() !== manifestKeywords.toString() ? userKeywords : null
 
     // If the manifest does not exist, generate a "fake" manifest data URL
     if (!manifestExists) {
       manifest = {
diff --git a/extension/src/sites/launch.js b/extension/src/sites/launch.js
index 7c489fa..986c7a9 100644
--- a/extension/src/sites/launch.js
+++ b/extension/src/sites/launch.js
@@ -1,9 +1,9 @@
 import '../utils/errors'
 import '../utils/i18nHtml'
 
-import { launchSite, obtainSiteList, obtainUrls, PREF_LAUNCH_CURRENT_URL, setPopupSize } from '../utils'
+import { launchSite, obtainSiteList, obtainUrls, PREF_LAUNCH_CURRENT_URL, sanitizeString, setPopupSize } from '../utils'
 import { getMessage } from '../utils/i18n'
 
 async function createInstanceList () {
   // Obtain a current manifest URL
   const { manifestUrl, documentUrl } = await obtainUrls()
@@ -20,15 +20,16 @@ async function createInstanceList () {
   let settingsLaunchCurrentUrl = (await browser.storage.local.get(PREF_LAUNCH_CURRENT_URL))[PREF_LAUNCH_CURRENT_URL]
   settingsLaunchCurrentUrl = settingsLaunchCurrentUrl !== undefined ? settingsLaunchCurrentUrl : true
 
   // Create a list element for every instance with handler that launches it
   for (const site of sites) {
+    const name = sanitizeString(site.config.name || site.manifest.name || site.manifest.short_name)
     const url = settingsLaunchCurrentUrl ? documentUrl : undefined
 
     const siteElement = document.createElement('button')
     siteElement.classList.add(...['list-group-item', 'list-group-item-action'])
-    siteElement.innerText = site.config.name || site.manifest.name || site.manifest.short_name || new URL(site.manifest.scope).host
+    siteElement.innerText = name || new URL(site.manifest.scope).host
     siteElement.addEventListener('click', () => { launchSite(site, url) })
 
     listElement.append(siteElement)
   }
 
diff --git a/extension/src/sites/manage.js b/extension/src/sites/manage.js
index 70760a7..d4e8f6b 100644
--- a/extension/src/sites/manage.js
+++ b/extension/src/sites/manage.js
@@ -20,12 +20,14 @@ import {
   obtainSiteList,
   PREF_AUTO_LAUNCH_EXCLUSION,
   PREF_DEFAULT_PROFILE_TEMPLATE,
   PREF_DISPLAY_PAGE_ACTION,
   PREF_ENABLE_AUTO_LAUNCH,
-  PREF_LAUNCH_CURRENT_URL, PREF_LOCALE,
+  PREF_LAUNCH_CURRENT_URL,
+  PREF_LOCALE,
   PREF_SHOW_UPDATE_POPUP,
+  sanitizeString,
   setConfig,
   setPopupSize
 } from '../utils'
 import { getAllLocales, getCurrentLocale, getMessage } from '../utils/i18n'
 import { knownCategories } from './categories'
@@ -81,11 +83,12 @@ async function createSiteList () {
   if (!sites.length) emptyElement.classList.remove('d-none')
 
   // Create a list element for every instance with handlers for launching and editing
   for (const site of sites) {
     const siteElement = templateElement.content.firstElementChild.cloneNode(true)
-    const siteName = site.config.name || site.manifest.name || site.manifest.short_name || new URL(site.manifest.scope).host
+    const siteName = sanitizeString(site.config.name || site.manifest.name || site.manifest.short_name) || new URL(site.manifest.scope).host
+    const siteDescription = sanitizeString(site.config.description || site.manifest.description) || ''
     const siteIcon = site.config.icon_url || getIcon(buildIconList(site.manifest.icons), 64)
 
     const letterElement = siteElement.querySelector('#sites-list-template-letter')
     if (siteIcon) letterElement.classList.add('d-none')
     letterElement.setAttribute('data-letter', siteName[0])
@@ -104,11 +107,11 @@ async function createSiteList () {
     const titleElement = siteElement.querySelector('#sites-list-template-title')
     titleElement.innerText = siteName
     titleElement.removeAttribute('id')
 
     const descriptionElement = siteElement.querySelector('#sites-list-template-description')
-    descriptionElement.innerText = site.config.description || site.manifest.description || ''
+    descriptionElement.innerText = siteDescription
     descriptionElement.removeAttribute('id')
 
     const launchElement = siteElement.querySelector('#sites-list-template-launch')
     const launchElementTooltip = await getMessage('managePageAppListLaunch')
     launchElement.addEventListener('click', () => { launchSite(site) })
@@ -121,12 +124,12 @@ async function createSiteList () {
     editElement.addEventListener('click', async (event) => {
       const form = document.getElementById('web-app-form')
       const submit = document.getElementById('web-app-submit')
 
       // Set placeholders from manifest
-      document.getElementById('web-app-name').setAttribute('placeholder', site.manifest.name || site.manifest.short_name || new URL(site.manifest.scope).host)
-      document.getElementById('web-app-description').setAttribute('placeholder', site.manifest.description || '')
+      document.getElementById('web-app-name').setAttribute('placeholder', sanitizeString(site.manifest.name || site.manifest.short_name) || new URL(site.manifest.scope).host)
+      document.getElementById('web-app-description').setAttribute('placeholder', sanitizeString(site.manifest.description) || '')
       document.getElementById('web-app-start-url').setAttribute('placeholder', site.manifest.start_url)
 
       // Set values from config
       document.getElementById('web-app-name').value = site.config.name || ''
       document.getElementById('web-app-description').value = site.config.description || ''
@@ -138,33 +141,35 @@ async function createSiteList () {
       const categoriesElement = document.getElementById('web-app-categories')
       categoriesElement.tagsInstance.resetSearchInput()
       categoriesElement.tagsInstance.reset()
 
       // Set categories from config or manifest
-      const categoriesList = site.config.categories?.length ? site.config.categories : site.manifest.categories
-      for (const category of categoriesList || []) categoriesElement.tagsInstance.addItem(category, category)
+      let categoriesList = site.config.categories?.length ? site.config.categories : site.manifest.categories
+      categoriesList = categoriesList?.map(item => sanitizeString(item)).filter(item => item) || []
+      for (const category of categoriesList) categoriesElement.tagsInstance.addItem(category, category)
 
       // Clear previous keywords
       const keywordsElement = document.getElementById('web-app-keywords')
       keywordsElement.tagsInstance.resetSearchInput()
       keywordsElement.tagsInstance.reset()
 
       // Set keywords from config or manifest
-      const keywordsList = site.config.keywords?.length ? site.config.keywords : site.manifest.keywords
-      for (const keyword of keywordsList || []) keywordsElement.tagsInstance.addItem(keyword, keyword)
+      let keywordsList = site.config.keywords?.length ? site.config.keywords : site.manifest.keywords
+      keywordsList = keywordsList?.map(item => sanitizeString(item)).filter(item => item) || []
+      for (const keyword of keywordsList) keywordsElement.tagsInstance.addItem(keyword, keyword)
 
       // Set site's profile from config
       const profilesElement = document.getElementById('web-app-profile')
       profilesElement.replaceChildren()
-      profilesElement.add(new Option(profiles[site.profile].name || site.profile, site.profile))
+      profilesElement.add(new Option(sanitizeString(profiles[site.profile].name) || site.profile, site.profile))
 
       // Create protocol handlers list and set enabled handlers
       // Currently not supported on macOS
       const platform = await browser.runtime.getPlatformInfo()
       if (platform.os !== 'mac') {
-        const possibleHandlers = new Set([...site.config.custom_protocol_handlers, ...site.manifest.protocol_handlers].map(handler => handler.protocol).sort())
-        const enabledHandlers = site.config.enabled_protocol_handlers
+        const possibleHandlers = new Set([...site.config.custom_protocol_handlers, ...site.manifest.protocol_handlers].map(handler => sanitizeString(handler.protocol)).filter(handler => handler).sort())
+        const enabledHandlers = site.config.enabled_protocol_handlers.map(handler => sanitizeString(handler)).filter(handler => handler)
 
         const handlersBox = document.getElementById('web-app-protocol-handlers-box')
         const handlersList = document.getElementById('web-app-protocol-handlers-list')
         handlersList.replaceChildren()
 
@@ -312,15 +317,15 @@ async function createSiteList () {
         const description = document.getElementById('web-app-description').value || null
 
         // Get categories and keywords based on user form input and site manifest
         // If the user list is identical to the manifest, ignore it, otherwise, set it as a user overwrite
         const userCategories = [...document.getElementById('web-app-categories').selectedOptions].map(option => option.value)
-        const manifestCategories = site.manifest.categories || []
+        const manifestCategories = site.manifest.categories?.map(item => sanitizeString(item)).filter(item => item) || []
         const categories = userCategories.toString() !== manifestCategories.toString() ? userCategories : null
 
         const userKeywords = [...document.getElementById('web-app-keywords').selectedOptions].map(option => option.value)
-        const manifestKeywords = site.manifest.keywords || []
+        const manifestKeywords = site.manifest.keywords?.map(item => sanitizeString(item)).filter(item => item) || []
         const keywords = userKeywords.toString() !== manifestKeywords.toString() ? userKeywords : null
 
         // Get list of enabled protocol handlers
         const enabledProtocolHandlers = [...document.querySelectorAll('.web-app-protocol-handler:checked')].map(check => check.value)
 
@@ -524,22 +529,22 @@ async function createProfileList () {
   // Create a list element for every instance with handlers for launching and editing
   for (const profile of profiles) {
     const profileElement = templateElement.content.firstElementChild.cloneNode(true)
 
     const nameElement = profileElement.querySelector('#profiles-list-template-name')
-    nameElement.innerText = profile.name || await getMessage('managePageProfileListUnnamed')
+    nameElement.innerText = sanitizeString(profile.name) || await getMessage('managePageProfileListUnnamed')
     nameElement.removeAttribute('id')
 
     const countElement = profileElement.querySelector('#profiles-list-template-count')
     countElement.innerText = `(${await getMessage('managePageProfileListCount', undefined, profile.sites.length)})`
     if (profile.sites.length === 0) {
       countElement.classList.add('text-opacity-50')
     }
     countElement.removeAttribute('id')
 
     const descriptionElement = profileElement.querySelector('#profiles-list-template-description')
-    descriptionElement.innerText = profile.description || ''
+    descriptionElement.innerText = sanitizeString(profile.description) || ''
     descriptionElement.removeAttribute('id')
 
     const editElement = profileElement.querySelector('#profiles-list-template-edit')
     const editElementTooltip = await getMessage('managePageProfileListEdit')
     editElement.addEventListener('click', async (event) => {
@@ -682,12 +687,12 @@ async function handleSearch () {
   const searchHandler = function (listElement) {
     document.getElementById('search-box').classList.remove('invisible')
 
     document.getElementById('search-input').oninput = function () {
       for (const item of document.getElementById(listElement).children) {
-        const itemName = item.querySelector('.list-group-item-name')?.innerText.toLowerCase()
-        const searchQuery = this.value.toLowerCase()
+        const itemName = sanitizeString(item.querySelector('.list-group-item-name')?.innerText.toLowerCase())
+        const searchQuery = sanitizeString(this.value.toLowerCase())
 
         if (!itemName) continue
         item.classList.toggle('d-none', itemName.indexOf(searchQuery) === -1)
       }
     }
diff --git a/extension/src/utils.js b/extension/src/utils.js
index 54db905..f5054d7 100644
--- a/extension/src/utils.js
+++ b/extension/src/utils.js
@@ -235,10 +235,22 @@ export function isProtocolSchemePermitted (scheme) {
   }
 
   return true
 }
 
+/**
+ * Removes all control characters from the string.
+ *
+ * @param {string?} string
+ *
+ * @returns {string|undefined}
+ */
+export function sanitizeString (string) {
+  // eslint-disable-next-line no-control-regex
+  return string?.replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
+}
+
 /**
  * Launches the site in the app browser.
  *
  * @param {{ulid: string}} site
  * @param {string} [url]
diff --git a/native/src/components/site.rs b/native/src/components/site.rs
index 70bd5fd..4312892 100644
--- a/native/src/components/site.rs
+++ b/native/src/components/site.rs
@@ -13,10 +13,11 @@ use web_app_manifest::types::{ImagePurpose, ImageSize, Url as ManifestUrl};
 pub use web_app_manifest::WebAppManifest as SiteManifest;
 
 use crate::components::runtime::Runtime;
 use crate::directories::ProjectDirs;
 use crate::storage::Config;
+use crate::utils::sanitize_string;
 
 const DOWNLOAD_ERROR: &str = "Failed to download web app manifest";
 const DATA_URL_ERROR: &str = "Failed to process web app manifest data URL";
 const PARSE_ERROR: &str = "Failed to parse web app manifest";
 const INVALID_URL: &str = "Web app without valid absolute URL is not possible";
@@ -243,34 +244,40 @@ impl Site {
         } else {
             unreachable!("{}", INVALID_URL)
         }
     }
 
-    /// First tries the user-specified name, then try manifest name
+    /// First tries the user-specified name, then tries manifest name
     /// and then short name. If no name is specified, uses the domain.
     pub fn name(&self) -> String {
-        self.config
-            .name
-            .as_ref()
-            .cloned()
-            .or_else(|| self.manifest.name.as_ref().cloned())
-            .or_else(|| self.manifest.short_name.as_ref().cloned())
-            .unwrap_or_else(|| self.domain())
+        sanitize_string(
+            &self
+                .config
+                .name
+                .as_ref()
+                .cloned()
+                .or_else(|| self.manifest.name.as_ref().cloned())
+                .or_else(|| self.manifest.short_name.as_ref().cloned())
+                .unwrap_or_else(|| self.domain()),
+        )
     }
 
-    /// First tries the user-specified description, then try manifest description.
+    /// First tries the user-specified description, then tries manifest description.
     /// If no description is specified, returns an empty string.
     pub fn description(&self) -> String {
-        self.config
-            .description
-            .as_ref()
-            .cloned()
-            .or_else(|| self.manifest.description.as_ref().cloned())
-            .unwrap_or_else(|| "".into())
+        sanitize_string(
+            &self
+                .config
+                .description
+                .as_ref()
+                .cloned()
+                .or_else(|| self.manifest.description.as_ref().cloned())
+                .unwrap_or_else(|| "".into()),
+        )
     }
 
-    /// First tries the user-specified icon, then try manifest icons.
+    /// First tries the user-specified icon, then tries manifest icons.
     pub fn icons(&self) -> Vec<IconResource> {
         match &self.config.icon_url {
             Some(icon) => vec![IconResource {
                 src: ManifestUrl::Absolute(icon.clone()),
                 sizes: [ImageSize::default()].iter().cloned().collect(),
@@ -286,25 +293,31 @@ impl Site {
     ///
     /// There is no fixed list of categories, but some known categories are converted
     /// to XDG menu categories on Linux and Apple App Store categories on macOS.
     ///
     /// First tries the user-specified categories, then try manifest categories.
-    pub fn categories(&self) -> &[String] {
+    pub fn categories(&self) -> Vec<String> {
         match &self.config.categories {
             Some(categories) => categories,
             None => &self.manifest.categories,
         }
+        .iter()
+        .map(|item| sanitize_string(item))
+        .collect()
     }
 
     /// Keywords can also be used for user organization and contain
     /// additional information that can be used to describe the web app.
     ///
     /// Keywords are used as additional search queries on Linux.
     ///
     /// First tries the user-specified keywords, then try manifest keywords.
-    pub fn keywords(&self) -> &[String] {
+    pub fn keywords(&self) -> Vec<String> {
         match &self.config.keywords {
             Some(keywords) => keywords,
             None => &self.manifest.keywords,
         }
+        .iter()
+        .map(|item| sanitize_string(item))
+        .collect()
     }
 }
diff --git a/native/src/connector/process.rs b/native/src/connector/process.rs
index 9e61da6..96d1d0d 100644
--- a/native/src/connector/process.rs
+++ b/native/src/connector/process.rs
@@ -73,11 +73,11 @@ impl Process for GetConfig {
 }
 
 impl Process for SetConfig {
     fn process(&self, connection: &Connection) -> Result<ConnectorResponse> {
         let mut storage = Storage::load(connection.dirs)?;
-        storage.config = self.0.to_owned();
+        self.0.clone_into(&mut storage.config);
         storage.write(connection.dirs)?;
         Ok(ConnectorResponse::ConfigSet)
     }
 }
 
diff --git a/native/src/console/profile.rs b/native/src/console/profile.rs
index 9a69a45..ffe898f 100644
--- a/native/src/console/profile.rs
+++ b/native/src/console/profile.rs
@@ -18,10 +18,11 @@ use crate::console::app::{
 use crate::console::{store_value, Run};
 use crate::directories::ProjectDirs;
 use crate::integrations;
 use crate::integrations::IntegrationUninstallArgs;
 use crate::storage::Storage;
+use crate::utils::sanitize_string;
 
 fn apply_profile_template(
     template: &Option<PathBuf>,
     profile: &Ulid,
     dirs: &ProjectDirs,
@@ -46,12 +47,15 @@ impl Run for ProfileListCommand {
         let storage = Storage::load(&dirs)?;
 
         for (_, profile) in storage.profiles {
             println!(
                 "{:=^60}\nDescription: {}\nID: {}",
-                format!(" {} ", profile.name.unwrap_or_else(|| "* Unnamed *".into())),
-                profile.description.unwrap_or_else(|| "* Nothing *".into()),
+                format!(
+                    " {} ",
+                    sanitize_string(&profile.name.unwrap_or_else(|| "* Unnamed *".into()))
+                ),
+                sanitize_string(&profile.description.unwrap_or_else(|| "* Nothing *".into())),
                 profile.ulid
             );
 
             if !profile.sites.is_empty() {
                 println!("\nApps:");
diff --git a/native/src/console/site.rs b/native/src/console/site.rs
index b2ee46f..5c44072 100644
--- a/native/src/console/site.rs
+++ b/native/src/console/site.rs
@@ -1,6 +1,5 @@
-use std::convert::TryInto;
 use std::fs::metadata;
 use std::io;
 use std::io::Write;
 
 use anyhow::{bail, Context, Result};
diff --git a/native/src/integrations/implementation/linux.rs b/native/src/integrations/implementation/linux.rs
index 8c300e2..a316488 100644
--- a/native/src/integrations/implementation/linux.rs
+++ b/native/src/integrations/implementation/linux.rs
@@ -16,10 +16,11 @@ use web_app_manifest::types::{ImagePurpose, ImageSize};
 
 use crate::components::site::Site;
 use crate::integrations::categories::XDG_CATEGORIES;
 use crate::integrations::utils::{download_icon, normalize_category_name, process_icons};
 use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
+use crate::utils::sanitize_string;
 
 const BASE_DIRECTORIES_ERROR: &str = "Failed to determine base system directories";
 const CONVERT_ICON_URL_ERROR: &str = "Failed to convert icon URL";
 const CONVERT_SHORTCUT_URL_ERROR: &str = "Failed to convert shortcut URL";
 const DOWNLOAD_ICON_ERROR: &str = "Failed to download icon";
@@ -203,11 +204,11 @@ fn create_desktop_entry(
 ) -> Result<()> {
     // Process some known manifest categories and reformat them into XDG names
     let mut categories = vec![];
     for category in args.site.categories() {
         // Normalize category name for easier matching
-        let category = normalize_category_name(category);
+        let category = normalize_category_name(&category);
 
         // Get the mapped XDG category based on the site categories
         if let Some(category) = XDG_CATEGORIES.get(&category) {
             categories.extend_from_slice(category);
         }
@@ -246,26 +247,27 @@ StartupWMClass={wmclass}
             output
         }),
         protocols = args.site.config.enabled_protocol_handlers.iter().fold(
             String::new(),
             |mut output, protocol| {
-                let _ = write!(output, "x-scheme-handler/{protocol};");
+                let _ = write!(output, "x-scheme-handler/{};", sanitize_string(protocol));
                 output
             }
         ),
         icon = &ids.classid,
         wmclass = &ids.classid,
         exe = &exe,
     );
 
     // Store all shortcuts
     for (i, shortcut) in args.site.manifest.shortcuts.iter().enumerate() {
+        let name = sanitize_string(&shortcut.name);
         let url: Url = shortcut.url.clone().try_into().context(CONVERT_SHORTCUT_URL_ERROR)?;
         let icon = format!("{}-{}", ids.classid, i);
 
         if args.update_icons {
-            store_icons(&icon, &shortcut.name, &shortcut.icons, data, args.client.unwrap())
+            store_icons(&icon, &name, &shortcut.icons, data, args.client.unwrap())
                 .context("Failed to store shortcut icons")?;
         }
 
         let action = format!(
             "
@@ -274,11 +276,11 @@ Name={name}
 Icon={icon}
 Exec={exe} site launch {siteid} --url \"{url}\"
 ",
             actionid = i,
             siteid = &ids.ulid,
-            name = &shortcut.name,
+            name = &name,
             icon = &icon,
             url = &url,
             exe = &exe,
         );
 
diff --git a/native/src/integrations/implementation/macos.rs b/native/src/integrations/implementation/macos.rs
index 9260148..16a1cad 100644
--- a/native/src/integrations/implementation/macos.rs
+++ b/native/src/integrations/implementation/macos.rs
@@ -26,10 +26,11 @@ use crate::integrations::utils::{
     generate_icon,
     normalize_category_name,
     sanitize_name,
 };
 use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
+use crate::utils::sanitize_string;
 
 const BASE_DIRECTORIES_ERROR: &str = "Failed to determine base system directories";
 const CONVERT_ICON_URL_ERROR: &str = "Failed to convert icon URL";
 const DOWNLOAD_ICON_ERROR: &str = "Failed to download icon";
 const PROCESS_ICON_ERROR: &str = "Failed to process icon";
@@ -442,13 +443,14 @@ fn create_app_bundle(args: &IntegrationInstallArgs) -> Result<()> {
         .site
         .config
         .enabled_protocol_handlers
         .iter()
         .map(|protocol| {
+            let protocol = sanitize_string(protocol);
             let mut handler = plist::dictionary::Dictionary::new();
             handler.insert("CFBundleURLName".into(), format!("{protocol} URL").into());
-            handler.insert("CFBundleURLSchemes".into(), vec![protocol.clone().into()].into());
+            handler.insert("CFBundleURLSchemes".into(), vec![protocol.into()].into());
             handler.into()
         })
         .collect::<Vec<plist::Value>>();
 
     // FIXME: Protocol handlers do not work, they need to be fixed in the future
diff --git a/native/src/integrations/implementation/portableapps.rs b/native/src/integrations/implementation/portableapps.rs
index 0ad6fe9..1254e76 100644
--- a/native/src/integrations/implementation/portableapps.rs
+++ b/native/src/integrations/implementation/portableapps.rs
@@ -9,10 +9,11 @@ use log::warn;
 use web_app_manifest::types::ImageSize;
 
 use crate::integrations::categories::PORTABLEAPPS_CATEGORIES;
 use crate::integrations::utils::{normalize_category_name, process_icons};
 use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
+use crate::utils::sanitize_string;
 
 #[derive(Debug, Clone, Copy)]
 struct PortableAppIcon {
     size: u32,
     format: &'static str,
@@ -140,11 +141,19 @@ fn create_appinfo(args: &IntegrationInstallArgs, appid: &str, appinfo: &Path) ->
     config.set("Dependencies", "Requires64bitOS", Some("yes".into()));
     config.set("Dependencies", "RequiresPortableApp", Some("PWAsForFirefoxPortable".into()));
     config.set("Control", "Start", Some("launch.vbs".into()));
     config.set("Control", "BaseAppID", Some(appid.into()));
 
-    let protocols = args.site.config.enabled_protocol_handlers.join(",");
+    let protocols = args
+        .site
+        .config
+        .enabled_protocol_handlers
+        .iter()
+        .map(|item| sanitize_string(item))
+        .collect::<Vec<_>>()
+        .join(",");
+
     config.set("Associations", "Protocols", Some(protocols));
     config.set("Associations", "ProtocolCommandLine", Some("--protocol \"%1\"".into()));
 
     config.write(appinfo)?;
     Ok(())
diff --git a/native/src/integrations/implementation/windows.rs b/native/src/integrations/implementation/windows.rs
index 95393bc..a813f88 100644
--- a/native/src/integrations/implementation/windows.rs
+++ b/native/src/integrations/implementation/windows.rs
@@ -1,6 +1,5 @@
-use std::convert::TryInto;
 use std::fs::{copy, create_dir_all, remove_dir_all, remove_file, rename};
 use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Result};
 use log::warn;
@@ -33,10 +32,11 @@ use winreg::enums::HKEY_CURRENT_USER;
 use winreg::RegKey;
 
 use crate::components::site::Site;
 use crate::integrations::utils::{process_icons, sanitize_name};
 use crate::integrations::{IntegrationInstallArgs, IntegrationUninstallArgs};
+use crate::utils::sanitize_string;
 
 const ADD_REMOVE_PROGRAMS_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall";
 const REGISTERED_APPLICATIONS_KEY: &str = r"Software\RegisteredApplications";
 const START_MENU_PROGRAMS_PATH: &str = r"Microsoft\Windows\Start Menu\Programs";
 const STARTUP_PROGRAMS_PATH: &str = r"Microsoft\Windows\Start Menu\Programs\Startup";
@@ -232,15 +232,16 @@ fn create_jump_list_tasks(
     // Create task collection and add tasks
     let collection: IObjectCollection = create_instance(&EnumerableObjectCollection)?;
 
     for (i, shortcut) in shortcuts.iter().enumerate() {
         let url: Url = shortcut.url.clone().try_into().context("Failed to convert shortcut URL")?;
-        let description = shortcut.description.clone().unwrap_or_else(|| "".into());
+        let name = sanitize_string(&shortcut.name);
+        let description = sanitize_string(shortcut.description.as_deref().unwrap_or(""));
         let icon = icons.join(format!("shortcut{i}.ico",));
 
         if args.update_icons {
-            store_icon(&shortcut.name, &shortcut.icons, &icon, args.client.unwrap())
+            store_icon(&name, &shortcut.icons, &icon, args.client.unwrap())
                 .context("Failed to store shortcut icon")?;
         }
 
         // Create shell link and property store instances
         let link: IShellLinkW = create_instance(&ShellLink)?;
@@ -260,11 +261,11 @@ fn create_jump_list_tasks(
             let variant = InitPropVariantFromStringVector(Some(&[PCWSTR(hstring.as_ptr())]))?;
             store.SetValue(&PKEY_AppUserModel_ID, &variant)?;
 
             // Set title property
             // Docs: https://docs.microsoft.com/en-us/windows/win32/properties/props-system-title
-            let hstring = HSTRING::from(&shortcut.name);
+            let hstring = HSTRING::from(&name);
             let variant = InitPropVariantFromStringVector(Some(&[PCWSTR(hstring.as_ptr())]))?;
             store.SetValue(&PKEY_Title, &variant)?;
 
             // Commit store and add it to collection
             store.Commit()?;
@@ -342,11 +343,11 @@ fn register_protocol_handlers(
     }
 
     // Add enabled protocol handlers
     for protocol in &args.site.config.enabled_protocol_handlers {
         associations
-            .set_value(protocol, &ids.regid)
+            .set_value(sanitize_string(protocol), &ids.regid)
             .context("Failed to set protocol registry key")?;
     }
 
     Ok(())
 }
diff --git a/native/src/integrations/utils.rs b/native/src/integrations/utils.rs
index ac6d83c..85e07fd 100644
--- a/native/src/integrations/utils.rs
+++ b/native/src/integrations/utils.rs
@@ -1,9 +1,8 @@
 #![allow(dead_code)]
 
 use std::cmp::Ordering;
-use std::convert::TryInto;
 use std::path::Path;
 
 use ab_glyph::{Font, FontRef, PxScale};
 use anyhow::{bail, Context, Result};
 use data_url::DataUrl;
diff --git a/native/src/utils.rs b/native/src/utils.rs
index 410b786..b2b94eb 100644
--- a/native/src/utils.rs
+++ b/native/src/utils.rs
@@ -104,5 +104,10 @@ pub(crate) fn construct_certificates_and_client(
         danger_accept_invalid_certs,
         danger_accept_invalid_hostnames,
     )
     .context(CLIENT_CONSTRUCT_ERROR)
 }
+
+/// Remove all control characters from the string.
+pub fn sanitize_string(string: &str) -> String {
+    string.chars().filter(|char| !char.is_control()).collect()
+}
diff --git a/native/userchrome/profile/chrome/pwa/content/browser.jsm b/native/userchrome/profile/chrome/pwa/content/browser.jsm
index 45506cb..5d560a3 100644
--- a/native/userchrome/profile/chrome/pwa/content/browser.jsm
+++ b/native/userchrome/profile/chrome/pwa/content/browser.jsm
@@ -4,10 +4,11 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   applySystemIntegration: 'resource://pwa/utils/systemIntegration.jsm',
   buildIconList: 'resource://pwa/utils/systemIntegration.jsm',
   sendNativeMessage: 'resource://pwa/utils/nativeMessaging.jsm',
   hookFunction: 'resource://pwa/utils/hookFunction.jsm',
   xPref: 'resource://pwa/utils/xPref.jsm',
+  sanitizeString: 'resource://pwa/utils/common.jsm',
 });
 XPCOMUtils.defineLazyServiceGetter(this, 'ioService', '@mozilla.org/network/io-service;1', Ci.nsIIOService);
 XPCOMUtils.defineLazyServiceGetter(this, 'WindowsUIUtils', '@mozilla.org/windows-ui-utils;1', Ci.nsIWindowsUIUtils);
 
 class PwaBrowser {
@@ -82,11 +83,11 @@ class PwaBrowser {
     // Set initial favicon and title to the site's static info
     const siteIcons = buildIconList(window.gFFPWASiteConfig?.manifest.icons || []);
     const siteIcon = siteIcons.find(icon => icon.size >= 32) || siteIcons[siteIcons.length - 1];
     if (siteIcon) tabIconImage.setAttribute('src', siteIcon.icon.src);
 
-    const siteName = window.gFFPWASiteConfig?.config.name || window.gFFPWASiteConfig?.manifest.name || window.gFFPWASiteConfig?.manifest.short_name
+    const siteName = sanitizeString(window.gFFPWASiteConfig?.config.name || window.gFFPWASiteConfig?.manifest.name || window.gFFPWASiteConfig?.manifest.short_name) || new URL(site.manifest.scope).host;
     tabLabel.replaceChildren(siteName);
     document.title = siteName;
 
     // Sync current tab favicon and title with custom info elements
     // This can be disabled by user using our preferences
@@ -350,11 +351,11 @@ class PwaBrowser {
       // If the protocol handler is already registered, just return early
       // We only allow one handler (either manifest or custom) per protocol scheme
       const existingHandlers = new Set([
         ...window.gFFPWASiteConfig.config.custom_protocol_handlers,
         ...window.gFFPWASiteConfig.manifest.protocol_handlers
-      ].map(handler => handler.protocol).sort());
+      ].map(handler => sanitizeString(handler.protocol)).filter(handler => handler).sort());
       if (existingHandlers.has(protocol)) return;
 
       // Now ask the user and provide the proper callback
       const message = this._getFormattedString('addProtocolHandlerMessage', [url.host, protocol,]);
 
diff --git a/native/userchrome/profile/chrome/pwa/content/macosHiddenWindow.jsm b/native/userchrome/profile/chrome/pwa/content/macosHiddenWindow.jsm
index af1326a..d534a02 100644
--- a/native/userchrome/profile/chrome/pwa/content/macosHiddenWindow.jsm
+++ b/native/userchrome/profile/chrome/pwa/content/macosHiddenWindow.jsm
@@ -1,5 +1,7 @@
+const { sanitizeString } = ChromeUtils.import('resource://pwa/utils/common.jsm');
+
 function OpenPwaShortcut(url) {
   switchToTabHavingURI(url, true);
 }
 
 class MacOSHiddenWindow {
@@ -17,12 +19,12 @@ class MacOSHiddenWindow {
     });
 
     shortcuts.forEach((item) => {
       const menuItem = document.createXULElement('menuitem');
 
-      menuItem.setAttribute('oncommand', `OpenPwaShortcut("${item.url}");`);
-      menuItem.setAttribute('label', item.name);
+      menuItem.setAttribute('oncommand', `OpenPwaShortcut("${sanitizeString(item.url)}");`);
+      menuItem.setAttribute('label', sanitizeString(item.name));
 
       dockMenu.appendChild(menuItem);
     });
   }
 }
diff --git a/native/userchrome/profile/chrome/pwa/utils/common.jsm b/native/userchrome/profile/chrome/pwa/utils/common.jsm
new file mode 100644
index 0000000..9d6d0c4
--- /dev/null
+++ b/native/userchrome/profile/chrome/pwa/utils/common.jsm
@@ -0,0 +1,12 @@
+const EXPORTED_SYMBOLS = ['sanitizeString'];
+
+/**
+ * Removes all control characters from the string.
+ *
+ * @param {string?} string
+ *
+ * @returns {string|undefined}
+ */
+function sanitizeString (string) {
+  return string?.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
+}
diff --git a/native/userchrome/profile/chrome/pwa/utils/systemIntegration.jsm b/native/userchrome/profile/chrome/pwa/utils/systemIntegration.jsm
index e3ad4a7..1c3ab9b 100644
--- a/native/userchrome/profile/chrome/pwa/utils/systemIntegration.jsm
+++ b/native/userchrome/profile/chrome/pwa/utils/systemIntegration.jsm
@@ -5,10 +5,11 @@ const Services = globalThis.Services || ChromeUtils.import('resource://gre/modul
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: 'resource://gre/modules/AppConstants.jsm',
   ImageTools: 'resource:///modules/ssb/ImageTools.jsm',
   NetUtil: 'resource://gre/modules/NetUtil.jsm',
   xPref: 'resource://pwa/utils/xPref.jsm',
+  sanitizeString: 'resource://pwa/utils/common.jsm',
 });
 XPCOMUtils.defineLazyServiceGetter(this, 'ImgTools', '@mozilla.org/image/tools;1', Ci.imgITools);
 XPCOMUtils.defineLazyServiceGetter(this, 'WinUIUtils', '@mozilla.org/windows-ui-utils;1', Ci.nsIWindowsUIUtils);
 XPCOMUtils.defineLazyServiceGetter(this, 'WinTaskbar', '@mozilla.org/windows-taskbar;1', Ci.nsIWinTaskbar);
 
@@ -180,11 +181,12 @@ function setWindowColors (window, site) {
  * @param {object} site - Site config for which integration should be used
  */
 function applySystemIntegration (window, site) {
   // Set title only on the main browser chrome window
   if (window.location.href === AppConstants.BROWSER_CHROME_URL) {
-    window.document.title = site.config.name || site.manifest.name || site.manifest.short_name;
+    const name = sanitizeString(site.config.name || site.manifest.name || site.manifest.short_name);
+    window.document.title = name || new URL(site.manifest.scope).host;
   }
 
   window.document.documentElement.setAttribute('icon', `FFPWA-${site.ulid}`);
   window.document.documentElement.setAttribute('windowclass', `FFPWA-${site.ulid}`);
   window.document.documentElement.setAttribute('windowname', `FFPWA-${site.ulid}`);

Vulnerable code:   for (const category of manifest?.categories || []) categoriesElement.tagsInstance.addItem(category, category)
  for (const keyword of manifest?.keywords || []) keywordsElement.tagsInstance.addItem(keyword, keyword)
    const manifestCategories = manifest?.categories || []
    const manifestKeywords = manifest?.keywords || []
import { launchSite, obtainSiteList, obtainUrls, PREF_LAUNCH_CURRENT_URL, setPopupSize } from '../utils'
    siteElement.innerText = site.config.name || site.manifest.name || site.manifest.short_name || new URL(site.manifest.scope).host
  PREF_LAUNCH_CURRENT_URL, PREF_LOCALE,
    const siteName = site.config.name || site.manifest.name || site.manifest.short_name || new URL(site.manifest.scope).host
    descriptionElement.innerText = site.config.description || site.manifest.description || ''
      document.getElementById('web-app-name').setAttribute('placeholder', site.manifest.name || site.manifest.short_name || new URL(site.manifest.scope).host)
      document.getElementById('web-app-description').setAttribute('placeholder', site.manifest.description || '')
      const categoriesList = site.config.categories?.length ? site.config.categories : site.manifest.categories
      for (const category of categoriesList || []) categoriesElement.tagsInstance.addItem(category, category)
      const keywordsList = site.config.keywords?.length ? site.config.keywords : site.manifest.keywords
      for (const keyword of keywordsList || []) keywordsElement.tagsInstance.addItem(keyword, keyword)
      profilesElement.add(new Option(profiles[site.profile].name || site.profile, site.profile))
        const possibleHandlers = new Set([...site.config.custom_protocol_handlers, ...site.manifest.protocol_handlers].map(handler => handler.protocol).sort())
        const enabledHandlers = site.config.enabled_protocol_handlers
        const manifestCategories = site.manifest.categories || []
        const manifestKeywords = site.manifest.keywords || []
    nameElement.innerText = profile.name || await getMessage('managePageProfileListUnnamed')
    descriptionElement.innerText = profile.description || ''
        const itemName = item.querySelector('.list-group-item-name')?.innerText.toLowerCase()
        const searchQuery = this.value.toLowerCase()
    /// First tries the user-specified name, then try manifest name
        self.config
            .name
            .as_ref()
            .cloned()
            .or_else(|| self.manifest.name.as_ref().cloned())
            .or_else(|| self.manifest.short_name.as_ref().cloned())
            .unwrap_or_else(|| self.domain())
    /// First tries the user-specified description, then try manifest description.
        self.config
            .description
            .as_ref()
            .cloned()
            .or_else(|| self.manifest.description.as_ref().cloned())
            .unwrap_or_else(|| "".into())
    /// First tries the user-specified icon, then try manifest icons.
    pub fn categories(&self) -> &[String] {
    pub fn keywords(&self) -> &[String] {
        storage.config = self.0.to_owned();
                format!(" {} ", profile.name.unwrap_or_else(|| "* Unnamed *".into())),
                profile.description.unwrap_or_else(|| "* Nothing *".into()),
use std::convert::TryInto;
        let category = normalize_category_name(category);
                let _ = write!(output, "x-scheme-handler/{protocol};");
            store_icons(&icon, &shortcut.name, &shortcut.icons, data, args.client.unwrap())
            name = &shortcut.name,
            handler.insert("CFBundleURLSchemes".into(), vec![protocol.clone().into()].into());
    let protocols = args.site.config.enabled_protocol_handlers.join(",");
use std::convert::TryInto;
        let description = shortcut.description.clone().unwrap_or_else(|| "".into());
            store_icon(&shortcut.name, &shortcut.icons, &icon, args.client.unwrap())
            let hstring = HSTRING::from(&shortcut.name);
            .set_value(protocol, &ids.regid)
use std::convert::TryInto;
    const siteName = window.gFFPWASiteConfig?.config.name || window.gFFPWASiteConfig?.manifest.name || window.gFFPWASiteConfig?.manifest.short_name
      ].map(handler => handler.protocol).sort());
      menuItem.setAttribute('oncommand', `OpenPwaShortcut("${item.url}");`);
      menuItem.setAttribute('label', item.name);
    window.document.title = site.config.name || site.manifest.name || site.manifest.short_name;
Fixed code:   sanitizeString,
  const categoriesList = manifest?.categories?.map(item => sanitizeString(item)).filter(item => item) || []
  for (const category of categoriesList) categoriesElement.tagsInstance.addItem(category, category)
  const keywordsList = manifest?.keywords?.map(item => sanitizeString(item)).filter(item => item) || []
  for (const keyword of keywordsList) keywordsElement.tagsInstance.addItem(keyword, keyword)
    const manifestCategories = manifest?.categories?.map(item => sanitizeString(item)).filter(item => item) || []
    const manifestKeywords = manifest?.keywords?.map(item => sanitizeString(item)).filter(item => item) || []
import { launchSite, obtainSiteList, obtainUrls, PREF_LAUNCH_CURRENT_URL, sanitizeString, setPopupSize } from '../utils'
    const name = sanitizeString(site.config.name || site.manifest.name || site.manifest.short_name)
    siteElement.innerText = name || new URL(site.manifest.scope).host
  PREF_LAUNCH_CURRENT_URL,
  PREF_LOCALE,
  sanitizeString,
    const siteName = sanitizeString(site.config.name || site.manifest.name || site.manifest.short_name) || new URL(site.manifest.scope).host
    const siteDescription = sanitizeString(site.config.description || site.manifest.description) || ''
    descriptionElement.innerText = siteDescription
      document.getElementById('web-app-name').setAttribute('placeholder', sanitizeString(site.manifest.name || site.manifest.short_name) || new URL(site.manifest.scope).host)
      document.getElementById('web-app-description').setAttribute('placeholder', sanitizeString(site.manifest.description) || '')
      let categoriesList = site.config.categories?.length ? site.config.categories : site.manifest.categories
      categoriesList = categoriesList?.map(item => sanitizeString(item)).filter(item => item) || []
      for (const category of categoriesList) categoriesElement.tagsInstance.addItem(category, category)
      let keywordsList = site.config.keywords?.length ? site.config.keywords : site.manifest.keywords
      keywordsList = keywordsList?.map(item => sanitizeString(item)).filter(item => item) || []
      for (const keyword of keywordsList) keywordsElement.tagsInstance.addItem(keyword, keyword)
      profilesElement.add(new Option(sanitizeString(profiles[site.profile].name) || site.profile, site.profile))
        const possibleHandlers = new Set([...site.config.custom_protocol_handlers, ...site.manifest.protocol_handlers].map(handler => sanitizeString(handler.protocol)).filter(handler => handler).sort())
        const enabledHandlers = site.config.enabled_protocol_handlers.map(handler => sanitizeString(handler)).filter(handler => handler)
        const manifestCategories = site.manifest.categories?.map(item => sanitizeString(item)).filter(item => item) || []
        const manifestKeywords = site.manifest.keywords?.map(item => sanitizeString(item)).filter(item => item) || []
    nameElement.innerText = sanitizeString(profile.name) || await getMessage('managePageProfileListUnnamed')
    descriptionElement.innerText = sanitizeString(profile.description) || ''
        const itemName = sanitizeString(item.querySelector('.list-group-item-name')?.innerText.toLowerCase())
        const searchQuery = sanitizeString(this.value.toLowerCase())
/**
 * Removes all control characters from the string.
 *
 * @param {string?} string
 *
 * @returns {string|undefined}
 */
export function sanitizeString (string) {
  // eslint-disable-next-line no-control-regex
  return string?.replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
}

use crate::utils::sanitize_string;
    /// First tries the user-specified name, then tries manifest name
        sanitize_string(
            &self
                .config
                .name
                .as_ref()
                .cloned()
                .or_else(|| self.manifest.name.as_ref().cloned())
                .or_else(|| self.manifest.short_name.as_ref().cloned())
                .unwrap_or_else(|| self.domain()),
        )
    /// First tries the user-specified description, then tries manifest description.
        sanitize_string(
            &self
                .config
                .description
                .as_ref()
                .cloned()
                .or_else(|| self.manifest.description.as_ref().cloned())
                .unwrap_or_else(|| "".into()),
        )
    /// First tries the user-specified icon, then tries manifest icons.
    pub fn categories(&self) -> Vec<String> {
        .iter()
        .map(|item| sanitize_string(item))
        .collect()
    pub fn keywords(&self) -> Vec<String> {
        .iter()
        .map(|item| sanitize_string(item))
        .collect()
        self.0.clone_into(&mut storage.config);
use crate::utils::sanitize_string;
                format!(
                    " {} ",
                    sanitize_string(&profile.name.unwrap_or_else(|| "* Unnamed *".into()))
                ),
                sanitize_string(&profile.description.unwrap_or_else(|| "* Nothing *".into())),
use crate::utils::sanitize_string;
        let category = normalize_category_name(&category);
                let _ = write!(output, "x-scheme-handler/{};", sanitize_string(protocol));
        let name = sanitize_string(&shortcut.name);
            store_icons(&icon, &name, &shortcut.icons, data, args.client.unwrap())
            name = &name,
use crate::utils::sanitize_string;
            let protocol = sanitize_string(protocol);
            handler.insert("CFBundleURLSchemes".into(), vec![protocol.into()].into());
use crate::utils::sanitize_string;
    let protocols = args
        .site
        .config
        .enabled_protocol_handlers
        .iter()
        .map(|item| sanitize_string(item))
        .collect::<Vec<_>>()
        .join(",");

use crate::utils::sanitize_string;
        let name = sanitize_string(&shortcut.name);
        let description = sanitize_string(shortcut.description.as_deref().unwrap_or(""));
            store_icon(&name, &shortcut.icons, &icon, args.client.unwrap())
            let hstring = HSTRING::from(&name);
            .set_value(sanitize_string(protocol), &ids.regid)

/// Remove all control characters from the string.
pub fn sanitize_string(string: &str) -> String {
    string.chars().filter(|char| !char.is_control()).collect()
}
  sanitizeString: 'resource://pwa/utils/common.jsm',
    const siteName = sanitizeString(window.gFFPWASiteConfig?.config.name || window.gFFPWASiteConfig?.manifest.name || window.gFFPWASiteConfig?.manifest.short_name) || new URL(site.manifest.scope).host;
      ].map(handler => sanitizeString(handler.protocol)).filter(handler => handler).sort());
const { sanitizeString } = ChromeUtils.import('resource://pwa/utils/common.jsm');

      menuItem.setAttribute('oncommand', `OpenPwaShortcut("${sanitizeString(item.url)}");`);
      menuItem.setAttribute('label', sanitizeString(item.name));
const EXPORTED_SYMBOLS = ['sanitizeString'];

/**
 * Removes all control characters from the string.
 *
 * @param {string?} string
 *
 * @returns {string|undefined}
 */
function sanitizeString (string) {
  return string?.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
}
  sanitizeString: 'resource://pwa/utils/common.jsm',
    const name = sanitizeString(site.config.name || site.manifest.name || site.manifest.short_name);
    window.document.title = name || new URL(site.manifest.scope).host;