CVE-2024-32986 (Rust, published 2024-05-03T10:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/filips123/PWAsForFirefox
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 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.


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;