VYPR
Low severityNVD Advisory· Published Feb 12, 2026· Updated Feb 13, 2026

Solspace Freeform plugin affected by Stored Cross-Site Scripting (XSS) in Freeform Craft Plugin CP UI (builder/integrations)

CVE-2026-26188

Description

Solspace Freeform plugin for Craft CMS 5.x is a super flexible form-building tool. An authenticated, low-privilege user (able to create/edit forms) can inject arbitrary HTML/JS into the Craft Control Panel (CP) builder and integrations views. User-controlled form labels and integration metadata are rendered with dangerouslySetInnerHTML without sanitization, leading to stored XSS that executes when any admin views the builder/integration screens. This vulnerability is fixed in 5.14.7.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
solspace/craft-freeformPackagist
>= 5.0.0, < 5.14.75.14.7

Affected products

1

Patches

1
b9adad6cdf1e

fix(SFT-2567): resolve all code scanning alerts (#2342)

https://github.com/solspace/craft-freeformGustavs GūtmanisJan 21, 2026via ghsa
63 files changed · +8568 14772
  • composer.json+1 1 modified
    @@ -1,7 +1,7 @@
     {
       "name": "solspace/craft-freeform",
       "description": "The most flexible and user-friendly form building plugin!",
    -  "version": "5.14.6",
    +  "version": "5.14.6.1",
       "type": "craft-plugin",
       "authors": [
         {
    
  • package.json+9 8 modified
    @@ -14,8 +14,8 @@
         "axios": "^1.13.2",
         "globals": "^15.14.0",
         "install": "^0.13.0",
    -    "lerna": "^9.0.3",
         "dom-to-image": "^2.6.0",
    +    "dompurify": "^3.3.1",
         "qs": ">=6.14.1"
       },
       "devDependencies": {
    @@ -39,20 +39,21 @@
         "postcss": "^8.5.0",
         "prettier": "^3.4.2",
         "prettier-eslint": "^16.3.0",
    -    "typescript": "^5.7.3"
    +    "typescript": "^5.7.3",
    +    "concurrently": "^9.1.2"
       },
       "overrides": {
         "diff": "^8.0.3"
       },
       "scripts": {
    -    "dev": "lerna run dev",
    -    "client": "lerna run dev --scope=@ff/client",
    -    "front-end": "lerna run dev --scope='{@ff/scripts,@ff/styles}'",
    -    "build": "lerna run build",
    +    "dev": "concurrently -n client,scripts,styles \"npm run -w @ff/client dev\" \"npm run -w @ff/scripts dev\" \"npm run -w @ff/styles dev\"",
    +    "client": "npm run -w @ff/client dev",
    +    "front-end": "concurrently -n scripts,styles \"npm run -w @ff/scripts dev\" \"npm run -w @ff/styles dev\"",
    +    "build": "npm run -ws --if-present build",
         "format": "prettier --write '**/*.{ts,tsx,md,json,js,jsx,css}'",
         "format:verify": "prettier --list-different '**/*.{ts,tsx,md,json,js,jsx,css}'",
    -    "lint": "lerna run lint",
    -    "test": "lerna run test",
    +    "lint": "npm run -ws --if-present lint",
    +    "test": "npm run -ws --if-present test",
         "prepare": "husky"
       },
       "author": "Solspace, Inc.",
    
  • package-lock.json+8079 14412 modified
  • packages/client/src/app/components/elements/custom-dropdown/dropdown.options.tsx+6 1 modified
    @@ -1,5 +1,6 @@
     import React, { useEffect, useRef } from 'react';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     
     import CheckIcon from './check.svg';
     import type { DropdownProps } from './dropdown';
    @@ -95,7 +96,11 @@ export const Options: React.FC<Props> = ({
                     <LabelContainer>
                       {option.icon && option.icon}
                       <div>
    -                    <span dangerouslySetInnerHTML={{ __html: option.label }} />
    +                    <span
    +                      dangerouslySetInnerHTML={{
    +                        __html: sanitize(option.label),
    +                      }}
    +                    />
                       </div>
                     </LabelContainer>
     
    
  • packages/client/src/app/components/elements/custom-dropdown/dropdown.tsx+2 1 modified
    @@ -13,6 +13,7 @@ import { useOnKeypress } from '@ff-client/hooks/use-on-keypress';
     import type { OptionCollection } from '@ff-client/types/properties';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { PopUpPortal } from '../pop-up-portal';
     
    @@ -168,7 +169,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
             {showSelectedIcon && <Icon>{selectedOption?.icon}</Icon>}
             <span
               dangerouslySetInnerHTML={{
    -            __html: selectedOption?.label || translate(emptyOption),
    +            __html: sanitize(selectedOption?.label || translate(emptyOption)),
               }}
             />
     
    
  • packages/client/src/app/components/form-controls/control-types/ai-box/ai-box.preview.tsx+2 1 modified
    @@ -1,5 +1,6 @@
     import React from 'react';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { NoContent, PreviewWrapper } from '../table/table.preview.styles';
     
    @@ -18,7 +19,7 @@ export const AiBoxPreview: React.FC<Props> = ({ value }) => {
             <div
               style={{ lineHeight: '2.0' }}
               dangerouslySetInnerHTML={{
    -            __html: generateValue(value, '<mark>...</mark>'),
    +            __html: sanitize(generateValue(value, '<mark>...</mark>')),
               }}
             />
           </PreviewContainer>
    
  • packages/client/src/app/components/form-controls/control-types/attributes/attributes.editor.tsx+5 2 modified
    @@ -8,6 +8,7 @@ import type {
     } from '@ff-client/types/properties';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { useCellNavigation } from '../../hooks/use-cell-navigation';
     import {
    @@ -203,8 +204,10 @@ export const AttributesEditor: React.FC<Props> = ({
             <HelpText>
               <span
                 dangerouslySetInnerHTML={{
    -              __html: translate(
    -                'Press <b>enter</b> while editing a cell to add a new row.'
    +              __html: sanitize(
    +                translate(
    +                  'Press <b>enter</b> while editing a cell to add a new row.'
    +                )
                   ),
                 }}
               />
    
  • packages/client/src/app/components/form-controls/control-types/calculation-box/calculation-box.preview.tsx+2 1 modified
    @@ -1,5 +1,6 @@
     import React from 'react';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { NoContent, PreviewWrapper } from '../table/table.preview.styles';
     
    @@ -18,7 +19,7 @@ export const CalculationBoxPreview: React.FC<Props> = ({ value }) => {
             <div
               style={{ lineHeight: '2.0' }}
               dangerouslySetInnerHTML={{
    -            __html: generateValue(value, '<mark>...</mark>'),
    +            __html: sanitize(generateValue(value, '<mark>...</mark>')),
               }}
             />
           </PreviewContainer>
    
  • packages/client/src/app/components/form-controls/control-types/namespaced/cards/editor/cards.editor.tsx+5 2 modified
    @@ -5,6 +5,7 @@ import type { Field } from '@editor/store/slices/layout/fields';
     import { useTranslations } from '@editor/store/slices/translations/translations.hooks';
     import type { CardsProperty } from '@ff-client/types/properties';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     import Sortable from 'sortablejs';
     
     import { addCard } from '../cards.operations';
    @@ -97,8 +98,10 @@ export const CardsEditor: React.FC<Props> = ({
           <HelpText>
             <span
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              'Press <b>enter</b> while editing a cell to add a new row.'
    +            __html: sanitize(
    +              translate(
    +                'Press <b>enter</b> while editing a cell to add a new row.'
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/components/form-controls/control-types/namespaced/notifications/notification-template/modal/inputs/html-body/tokens/components/item.tsx+2 1 modified
    @@ -1,6 +1,7 @@
     import React, { useEffect, useRef } from 'react';
     import type { Suggestion } from '@ff-client/types/notifications';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     
     import { ItemWrapper } from './item.styles';
     
    @@ -26,7 +27,7 @@ export const Item: React.FC<Props> = ({ item, onClick }) => {
           ref={ref}
           className={classes(item?.active && 'active')}
           onClick={() => onClick?.(item)}
    -      dangerouslySetInnerHTML={{ __html: item.shortName }}
    +      dangerouslySetInnerHTML={{ __html: sanitize(item.shortName) }}
         />
       );
     };
    
  • packages/client/src/app/components/form-controls/control-types/namespaced/notifications/notification-template/modal/inputs/text-tokens/text-tokens.tsx+8 7 modified
    @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
     import { ControlBlock } from '@components/form-controls/control.block';
     import { useAppStore } from '@editor/store';
     import type { Suggestion } from '@ff-client/types/notifications';
    +import { sanitize } from 'dompurify';
     
     import type { InputControl } from '../../template.modal.types';
     import { hide, show } from '../html-body/tokens/operations/dropdown';
    @@ -64,7 +65,7 @@ export const TextTokens: FC<InputControl> = (props) => {
             const span = document.createElement('span');
             span.contentEditable = 'false';
             span.dataset.freeformToken = item.token;
    -        span.innerHTML = item.name;
    +        span.innerHTML = sanitize(item.name);
     
             tokenRange.deleteContents();
             tokenRange.insertNode(span);
    @@ -77,7 +78,7 @@ export const TextTokens: FC<InputControl> = (props) => {
             selection.addRange(newRange);
     
             // Update your state if needed
    -        onChange(wrapperRef.current?.innerHTML ?? '');
    +        onChange(sanitize(wrapperRef.current?.innerHTML ?? ''));
           },
           store,
           handlers: {
    @@ -119,13 +120,13 @@ export const TextTokens: FC<InputControl> = (props) => {
           const span = document.createElement('span');
           span.contentEditable = 'false';
           span.dataset.freeformToken = item.token;
    -      span.innerHTML = item.name;
    +      span.innerHTML = sanitize(item.name);
     
           const range = lastRangeRef.current;
           // if no range is selected, we insert at the end of the text
           if (!range) {
             wrapperRef.current?.appendChild(span);
    -        onChange(wrapperRef.current.innerHTML);
    +        onChange(sanitize(wrapperRef.current.innerHTML));
             return;
           }
     
    @@ -157,7 +158,7 @@ export const TextTokens: FC<InputControl> = (props) => {
           selection.addRange(newRange);
     
           // Update your state if needed
    -      onChange(wrapperRef.current?.innerHTML ?? '');
    +      onChange(sanitize(wrapperRef.current?.innerHTML ?? ''));
         },
         store,
         handlers: {
    @@ -203,7 +204,7 @@ export const TextTokens: FC<InputControl> = (props) => {
     
       useEffect(() => {
         if (wrapperRef.current && wrapperRef.current.innerHTML !== value) {
    -      wrapperRef.current.innerHTML = value;
    +      wrapperRef.current.innerHTML = sanitize(value);
         }
       }, [value]);
     
    @@ -217,7 +218,7 @@ export const TextTokens: FC<InputControl> = (props) => {
           }
     
           if (wrapperRef.current) {
    -        onChange(wrapperRef.current.innerHTML);
    +        onChange(sanitize(wrapperRef.current.innerHTML));
           }
         },
         [backend, onChange]
    
  • packages/client/src/app/components/form-controls/control-types/namespaced/notifications/recipients/recipients.tsx+5 2 modified
    @@ -4,6 +4,7 @@ import { Control } from '@components/form-controls/control';
     import type { ControlType } from '@components/form-controls/types';
     import type { RecipientsProperty } from '@ff-client/types/properties';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { RecipientsController } from './recipients.controller';
     
    @@ -20,8 +21,10 @@ const Recipients: React.FC<ControlType<RecipientsProperty>> = ({
           <HelpText>
             <span
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              'Press <b>enter</b> while focusing an input to add a new set of inputs.'
    +            __html: sanitize(
    +              translate(
    +                'Press <b>enter</b> while focusing an input to add a new set of inputs.'
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/components/form-controls/control-types/options/sources/custom/custom.editor.tsx+5 2 modified
    @@ -19,6 +19,7 @@ import { PreviewEditor } from '@components/form-controls/preview/previewable-com
     import { useDebounce } from '@ff-client/hooks/use-debounce';
     import { PropertyType } from '@ff-client/types/properties';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import type {
       ConfigurationProps,
    @@ -330,8 +331,10 @@ export const CustomEditor: React.FC<
           <HelpText>
             <span
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              'Press <b>enter</b> while editing a cell to add a new row.'
    +            __html: sanitize(
    +              translate(
    +                'Press <b>enter</b> while editing a cell to add a new row.'
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/components/form-controls/control-types/table/table.editor.tsx+5 2 modified
    @@ -29,6 +29,7 @@ import type {
       TableProperty,
     } from '@ff-client/types/properties';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { TableCheckboxEditor } from './editor/table.input.checkbox';
     import { TableDropdownEditor } from './editor/table.input.dropdown';
    @@ -195,8 +196,10 @@ export const TableEditor: React.FC<Props> = ({
           <HelpText>
             <span
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              'Press <b>enter</b> while editing a cell to add a new row.'
    +            __html: sanitize(
    +              translate(
    +                'Press <b>enter</b> while editing a cell to add a new row.'
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/components/form-controls/control-types/tabular-data/tabular-data.editor.tsx+5 2 modified
    @@ -19,6 +19,7 @@ import type {
       TabularDataProperty,
     } from '@ff-client/types/properties';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import {
       addRow,
    @@ -180,8 +181,10 @@ export const TabularDataEditor: React.FC<Props> = ({
           <HelpText>
             <span
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              'Press <b>enter</b> while editing a cell to add a new row.'
    +            __html: sanitize(
    +              translate(
    +                'Press <b>enter</b> while editing a cell to add a new row.'
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/components/form-controls/control-types/wysiwyg/wysiwyg.preview.tsx+2 1 modified
    @@ -1,5 +1,6 @@
     import React from 'react';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { NoContent, PreviewWrapper } from '../table/table.preview.styles';
     
    @@ -14,7 +15,7 @@ export const WysiwygPreview: React.FC<Props> = ({ value }) => {
         <PreviewWrapper data-edit={translate('Click to edit data')}>
           <PreviewContainer>
             {!value && <NoContent>{translate('Not configured yet')}</NoContent>}
    -        <div dangerouslySetInnerHTML={{ __html: value }} />
    +        <div dangerouslySetInnerHTML={{ __html: sanitize(value) }} />
           </PreviewContainer>
         </PreviewWrapper>
       );
    
  • packages/client/src/app/components/form-controls/control-types/wysiwyg/wysiwyg.tsx+2 1 modified
    @@ -3,6 +3,7 @@ import { ButtonGroup } from '@components/elements/button-group/button-group';
     import { Control } from '@components/form-controls/control';
     import type { ControlType } from '@components/form-controls/types';
     import type { WYSIWYGProperty } from '@ff-client/types/properties';
    +import { sanitize } from 'dompurify';
     
     import { WysiwygPlain } from './wysiwyg.plain';
     import { WysiwygRich } from './wysiwyg.rich';
    @@ -37,7 +38,7 @@ const Wysiwyg: React.FC<ControlType<WYSIWYGProperty>> = ({
         // When switching to simple mode, strip HTML tags
         if (newMode === EditorMode.Plain && value) {
           const tempDiv = document.createElement('div');
    -      tempDiv.innerHTML = value;
    +      tempDiv.innerHTML = sanitize(value);
           updateValue(tempDiv.textContent || '');
         }
       };
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/form-monitor/form-monitor.disable.tsx+5 2 modified
    @@ -14,6 +14,7 @@ import {
     import { QKForms } from '@ff-client/queries/forms';
     import translate from '@ff-client/utils/translations';
     import { useQueryClient } from '@tanstack/react-query';
    +import { sanitize } from 'dompurify';
     
     import { FormWrapper } from './form-monitor.action.modal.styles';
     
    @@ -117,8 +118,10 @@ export const DisableAndDeleteMonitoringModal: React.FC<ModalProps> = ({
               </div>
               <div
                 dangerouslySetInnerHTML={{
    -              __html: translate(
    -                'To disable monitoring and delete all data, please type <strong>CONFIRM</strong> in the box below:'
    +              __html: sanitize(
    +                translate(
    +                  'To disable monitoring and delete all data, please type <strong>CONFIRM</strong> in the box below:'
    +                )
                   ),
                 }}
               />
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/form-monitor/form-monitor.test.delete.tsx+5 2 modified
    @@ -13,6 +13,7 @@ import {
     } from '@ff-client/queries/form-monitor.mutations';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { FormWrapper } from './form-monitor.action.modal.styles';
     
    @@ -95,8 +96,10 @@ export const DeleteTestModal: React.FC<Props> = ({
                 <>
                   <div
                     dangerouslySetInnerHTML={{
    -                  __html: translate(
    -                    'To clear all test history, please type <strong>DELETE</strong> in the box below:'
    +                  __html: sanitize(
    +                    translate(
    +                      'To clear all test history, please type <strong>DELETE</strong> in the box below:'
    +                    )
                       ),
                     }}
                   />
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/form-settings/settings.sidebar.tsx+2 1 modified
    @@ -9,6 +9,7 @@ import { useQueryFormSettings } from '@ff-client/queries/forms';
     import classes from '@ff-client/utils/classes';
     import { hasErrors } from '@ff-client/utils/errors';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { useLastTab } from '../tabs.hooks';
     
    @@ -67,7 +68,7 @@ export const SettingsSidebar: React.FC = () => {
                     )}
                   >
                     <SectionIcon
    -                  dangerouslySetInnerHTML={{ __html: section.icon }}
    +                  dangerouslySetInnerHTML={{ __html: sanitize(section.icon) }}
                     />
                     {translate(section.label)}
                   </SectionLink>
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/integrations/sidebar/category/integration/integration.tsx+4 1 modified
    @@ -5,6 +5,7 @@ import { useLastTab } from '@editor/builder/tabs/tabs.hooks';
     import { integrationSelectors } from '@editor/store/slices/integrations/integrations.selectors';
     import type { Integration as IntegrationType } from '@ff-client/types/integrations';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     
     import CogIcon from './cog-icon.svg';
     import { Icon, Name, Status, Wrapper } from './integration.styles';
    @@ -40,7 +41,9 @@ export const Integration: React.FC<IntegrationType> = ({
                 <CogIcon />
               </Icon>
             )}
    -        {!!icon && <Icon dangerouslySetInnerHTML={{ __html: icon }} />}
    +        {!!icon && (
    +          <Icon dangerouslySetInnerHTML={{ __html: sanitize(icon) }} />
    +        )}
             <Name>{name}</Name>
             <Status
               $enabled={integration.enabled}
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/field-layout/field/cell/cell.styles.ts+9 0 modified
    @@ -1,4 +1,8 @@
     import { animated } from 'react-spring';
    +import { CardCell } from '@ff-client/styles/field-cells/cards';
    +import { RatingCell } from '@ff-client/styles/field-cells/rating';
    +import { StripeCell } from '@ff-client/styles/field-cells/stripe';
    +import { TableCell } from '@ff-client/styles/field-cells/table';
     import { borderRadius, colors, spacings } from '@ff-client/styles/variables';
     import styled from 'styled-components';
     
    @@ -227,6 +231,11 @@ export const FieldCellWrapper = styled.div`
           }
         }
       }
    +
    +  ${StripeCell}
    +  ${CardCell}
    +  ${RatingCell}
    +  ${TableCell}
     `;
     
     export const Row = styled.div`
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/field-layout/field/cell/cell.tsx+4 3 modified
    @@ -10,6 +10,7 @@ import { useFieldType } from '@ff-client/queries/field-types';
     import { Type } from '@ff-client/types/fields';
     import classes from '@ff-client/utils/classes';
     import { hasErrors } from '@ff-client/utils/errors';
    +import { sanitize } from 'dompurify';
     
     import { GroupFieldLayout } from '../../layout/group-field-layout/group-field-layout';
     
    @@ -87,7 +88,7 @@ export const FieldCell: React.FC<Props> = ({ field }) => {
                 </Icon>
                 <Icon
                   style={iconAnimation}
    -              dangerouslySetInnerHTML={{ __html: type.icon }}
    +              dangerouslySetInnerHTML={{ __html: sanitize(type.icon) }}
                 />
               </LabelIcon>
     
    @@ -110,12 +111,12 @@ export const FieldCell: React.FC<Props> = ({ field }) => {
               {noLabel ? (
                 <Row>
                   <HtmlPreviewElement
    -                dangerouslySetInnerHTML={{ __html: preview }}
    +                dangerouslySetInnerHTML={{ __html: sanitize(preview) }}
                   />
                   <FieldAssociationsBadges uid={uid} />
                 </Row>
               ) : (
    -            <div dangerouslySetInnerHTML={{ __html: preview }} />
    +            <div dangerouslySetInnerHTML={{ __html: sanitize(preview) }} />
               )}
             </>
           )}
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/field-list/implementations/base-fields/modal/modal.list-item.tsx+2 1 modified
    @@ -2,6 +2,7 @@ import React, { useRef } from 'react';
     import { useHover } from '@ff-client/hooks/use-hover';
     import { useFieldType } from '@ff-client/queries/field-types';
     import CrossIcon from '@ff-icons/actions/delete.svg';
    +import { sanitize } from 'dompurify';
     
     import { Icon, Name, Remove, Wrapper } from './modal.list-item.styles';
     
    @@ -23,7 +24,7 @@ export const FieldItem: React.FC<Props> = ({ typeClass }) => {
     
       return (
         <Wrapper data-id={typeClass} ref={fieldItemRef} title={name}>
    -      <Icon dangerouslySetInnerHTML={{ __html: icon }} />
    +      <Icon dangerouslySetInnerHTML={{ __html: sanitize(icon) }} />
           <Name>{name}</Name>
     
           {hovering && (
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/field-list/implementations/favorite-fields/modal/modal.editor.tsx+5 2 modified
    @@ -16,6 +16,7 @@ import type {
     } from '@ff-client/types/fields';
     import type { GenericValue } from '@ff-client/types/properties';
     import { type Property } from '@ff-client/types/properties';
    +import { sanitize } from 'dompurify';
     
     import { FavoriteFieldComponent } from './modal.editor.field';
     
    @@ -70,9 +71,11 @@ export const FavoritesEditor: React.FC<Props> = ({
       return (
         <>
           <Title>
    -        <Icon dangerouslySetInnerHTML={{ __html: type.icon }} />
    +        <Icon dangerouslySetInnerHTML={{ __html: sanitize(type.icon) }} />
             <span
    -          dangerouslySetInnerHTML={{ __html: values?.label || type.name }}
    +          dangerouslySetInnerHTML={{
    +            __html: sanitize(values?.label || type.name),
    +          }}
             />
           </Title>
           <RenderContextProvider size={'small'}>
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/field-list/implementations/favorite-fields/modal/modal.list-item.tsx+3 2 modified
    @@ -4,6 +4,7 @@ import { useHover } from '@ff-client/hooks/use-hover';
     import { useFieldType } from '@ff-client/queries/field-types';
     import type { FieldFavorite } from '@ff-client/types/fields';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     
     import { FieldListItem, Icon } from './modal.styles';
     
    @@ -40,8 +41,8 @@ export const FavoriteListItem: React.FC<Props> = ({
           onClick={onClick}
           className={classes(isActive && 'active', hasErrors && 'errors')}
         >
    -      <Icon dangerouslySetInnerHTML={{ __html: fieldType.icon }} />
    -      <span dangerouslySetInnerHTML={{ __html: label }} />
    +      <Icon dangerouslySetInnerHTML={{ __html: sanitize(fieldType.icon) }} />
    +      <span dangerouslySetInnerHTML={{ __html: sanitize(label) }} />
           <RemoveButton active={hovering} onClick={onDelete} />
         </FieldListItem>
       );
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/property-editor/editors/fields/field-properties.tsx+3 2 modified
    @@ -12,6 +12,7 @@ import {
     } from '@ff-client/queries/field-types';
     import { type Property } from '@ff-client/types/properties';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { CloseLink, Icon, Title } from '../../property-editor.styles';
     import { SectionBlock } from '../../section-block';
    @@ -70,7 +71,7 @@ export const FieldProperties: React.FC<{ uid: string }> = ({ uid }) => {
         return (
           <FieldPropertiesWrapper>
             <Title>
    -          <Icon dangerouslySetInnerHTML={{ __html: type.icon }} />
    +          <Icon dangerouslySetInnerHTML={{ __html: sanitize(type.icon) }} />
               <span>{translate(type.name)}</span>
             </Title>
             <SectionWrapper>
    @@ -89,7 +90,7 @@ export const FieldProperties: React.FC<{ uid: string }> = ({ uid }) => {
             <FavoriteButton field={field} />
           )}
           <Title>
    -        <Icon dangerouslySetInnerHTML={{ __html: type.icon }} />
    +        <Icon dangerouslySetInnerHTML={{ __html: sanitize(type.icon) }} />
             <span>{translate(type.name)}</span>
           </Title>
           <SectionWrapper>{sectionBlocks}</SectionWrapper>
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/layout/property-editor/section-block.tsx+4 1 modified
    @@ -1,5 +1,6 @@
     import type { PropsWithChildren, ReactNode } from 'react';
     import React from 'react';
    +import { sanitize } from 'dompurify';
     
     import {
       SectionBlockContainer,
    @@ -18,7 +19,9 @@ const renderIcon = (icon?: string | ReactNode): ReactNode => {
       }
     
       if (typeof icon === 'string') {
    -    return <SectionBlockIcon dangerouslySetInnerHTML={{ __html: icon }} />;
    +    return (
    +      <SectionBlockIcon dangerouslySetInnerHTML={{ __html: sanitize(icon) }} />
    +    );
       }
     
       return <SectionBlockIcon>{icon}</SectionBlockIcon>;
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/notifications/sidebar/items/item.tsx+2 1 modified
    @@ -5,6 +5,7 @@ import { notificationSelectors } from '@editor/store/slices/notifications/notifi
     import type { Notification } from '@ff-client/types/notifications';
     import classes from '@ff-client/utils/classes';
     import { hasErrors } from '@ff-client/utils/errors';
    +import { sanitize } from 'dompurify';
     
     import { Icon, Link, Name, Status } from './item.styles';
     
    @@ -26,7 +27,7 @@ export const NotificationItem: React.FC<Props> = ({
           to={`${uid}`}
           className={classes(hasErrors(errors) && 'errors', !enabled && 'inactive')}
         >
    -      {icon && <Icon dangerouslySetInnerHTML={{ __html: icon }} />}
    +      {icon && <Icon dangerouslySetInnerHTML={{ __html: sanitize(icon) }} />}
           <Name>{name}</Name>
           <Status $enabled={enabled} className={classes('status-dot')} />
         </Link>
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/rules/editor/field.editor.tsx+3 2 modified
    @@ -10,6 +10,7 @@ import { fieldRuleSelectors } from '@editor/store/slices/rules/fields/field-rule
     import { useQueryFormRules } from '@ff-client/queries/rules';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { CombinatorSelect } from '../conditions/combinator/combinator';
     import { DisplaySelect } from '../conditions/display/display';
    @@ -49,7 +50,7 @@ export const FieldRulesEditor: React.FC = () => {
                 loadingText={translate('Loading data')}
                 loading={isFetching}
               >
    -            <span dangerouslySetInnerHTML={{ __html: label }} />
    +            <span dangerouslySetInnerHTML={{ __html: sanitize(label) }} />
               </LoadingText>
             </Label>
             {!isFetching && (
    @@ -83,7 +84,7 @@ export const FieldRulesEditor: React.FC = () => {
               loadingText={translate('Loading data')}
               loading={isFetching}
             >
    -          <span dangerouslySetInnerHTML={{ __html: label }} />
    +          <span dangerouslySetInnerHTML={{ __html: sanitize(label) }} />
             </LoadingText>
           </Label>
           {!isFetching && (
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/rules/editor/page.editor.tsx+3 2 modified
    @@ -9,6 +9,7 @@ import { pageRuleActions } from '@editor/store/slices/rules/pages';
     import { pageRuleSelectors } from '@editor/store/slices/rules/pages/page-rules.selectors';
     import { useQueryFormRules } from '@ff-client/queries/rules';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { CombinatorSelect } from '../conditions/combinator/combinator';
     import { ConditionTable } from '../conditions/table/condition-table';
    @@ -47,7 +48,7 @@ export const PageRulesEditor: React.FC = () => {
                 loadingText={translate('Loading data')}
                 loading={isFetching}
               >
    -            <span dangerouslySetInnerHTML={{ __html: label }} />
    +            <span dangerouslySetInnerHTML={{ __html: sanitize(label) }} />
               </LoadingText>
             </Label>
             {!isFetching && (
    @@ -76,7 +77,7 @@ export const PageRulesEditor: React.FC = () => {
               loadingText={translate('Loading data')}
               loading={isFetching}
             >
    -          <span dangerouslySetInnerHTML={{ __html: label }} />
    +          <span dangerouslySetInnerHTML={{ __html: sanitize(label) }} />
             </LoadingText>
           </Label>
           {!isFetching && (
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/rules/editor/upsell.editor.tsx+7 4 modified
    @@ -7,6 +7,7 @@ import { colors, spacings } from '@ff-client/styles/variables';
     import type { FieldRule } from '@ff-client/types/rules';
     import { Combinator, Display, Operator } from '@ff-client/types/rules';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     import styled from 'styled-components';
     
     import { CombinatorSelect } from '../conditions/combinator/combinator';
    @@ -52,16 +53,18 @@ export const UpsellEditor: FC<Props> = ({ label }) => {
         <RulesEditorWrapper>
           <Label>
             <LoadingText>
    -          <span dangerouslySetInnerHTML={{ __html: label }} />
    +          <span dangerouslySetInnerHTML={{ __html: sanitize(label) }} />
             </LoadingText>
           </Label>
     
           <PreviewWrapper>
             <UpsellBanner
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              '<a href="{link}" target="_blank">Upgrade to Freeform Pro</a> to create conditional rules.',
    -              { link: Craft.getCpUrl('plugin-store/freeform') }
    +            __html: sanitize(
    +              translate(
    +                '<a href="{link}" target="_blank">Upgrade to Freeform Pro</a> to create conditional rules.',
    +                { link: Craft.getCpUrl('plugin-store/freeform') }
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/pages/forms/edit/builder/tabs/rules/sidebar/field/field.tsx+3 2 modified
    @@ -12,6 +12,7 @@ import { useFieldType } from '@ff-client/queries/field-types';
     import type { PageButtonType } from '@ff-client/types/rules';
     import { operatorTypes } from '@ff-client/types/rules';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     
     import { Layout } from '../layout/layout';
     
    @@ -91,10 +92,10 @@ export const Field: React.FC<Props> = ({ field }) => {
           )}
         >
           <FieldInfo>
    -        <Icon dangerouslySetInnerHTML={{ __html: type?.icon }} />
    +        <Icon dangerouslySetInnerHTML={{ __html: sanitize(type?.icon) }} />
             <Label
               dangerouslySetInnerHTML={{
    -            __html: field.properties.label || type?.name,
    +            __html: sanitize(field.properties.label || type?.name),
               }}
             />
           </FieldInfo>
    
  • packages/client/src/app/pages/forms/list/modals/modal.form.delete.tsx+5 2 modified
    @@ -15,6 +15,7 @@ import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
     import { useQueryClient } from '@tanstack/react-query';
     import axios from 'axios';
    +import { sanitize } from 'dompurify';
     
     import { FormWrapper } from './modal.form.styles';
     
    @@ -89,8 +90,10 @@ export const DeleteFormModal: React.FC<ModalContainerProps> = ({
             </div>
             <div
               dangerouslySetInnerHTML={{
    -            __html: translate(
    -              'To delete this form, please type <strong>DELETE</strong> in the box below:'
    +            __html: sanitize(
    +              translate(
    +                'To delete this form, please type <strong>DELETE</strong> in the box below:'
    +              )
                 ),
               }}
             />
    
  • packages/client/src/app/pages/forms/list/notices/notices.tsx+9 6 modified
    @@ -3,6 +3,7 @@ import React from 'react';
     import config from '@config/freeform/freeform.config';
     import translate from '@ff-client/utils/translations';
     import { generateUrl } from '@ff-client/utils/urls';
    +import { sanitize } from 'dompurify';
     
     import CircleIcon from './icons/circle.icon.svg';
     import DeleteIcon from './icons/delete.icon.svg';
    @@ -62,12 +63,14 @@ export const Notices: React.FC = () => {
               </Icon>
               <Message
                 dangerouslySetInnerHTML={{
    -              __html: translate(
    -                'There are currently <a href="{link}">{errors} logged errors</a> in the Freeform error log files.',
    -                {
    -                  link: generateUrl('settings/error-log'),
    -                  errors: data.errors,
    -                }
    +              __html: sanitize(
    +                translate(
    +                  'There are currently <a href="{link}">{errors} logged errors</a> in the Freeform error log files.',
    +                  {
    +                    link: generateUrl('settings/error-log'),
    +                    errors: data.errors,
    +                  }
    +                )
                   ),
                 }}
               />
    
  • packages/client/src/app/pages/forms/list/views/grid/grid.tsx+6 3 modified
    @@ -5,6 +5,7 @@ import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
     import EditIcon from '@ff-icons/actions/edit.svg';
     import axios from 'axios';
    +import { sanitize } from 'dompurify';
     import Sortable from 'sortablejs';
     
     import { useEditGroupModal } from '../../modals/hooks/use-edit-group-modal';
    @@ -136,9 +137,11 @@ export const FormGrid: React.FC = () => {
               {isExpressEdition && (
                 <div
                   dangerouslySetInnerHTML={{
    -                __html: translate(
    -                  'Need more forms? <a href="{link}" target="_blank">Upgrade to Lite or Pro</a>.',
    -                  { link: Craft.getCpUrl('plugin-store/freeform') }
    +                __html: sanitize(
    +                  translate(
    +                    'Need more forms? <a href="{link}" target="_blank">Upgrade to Lite or Pro</a>.',
    +                    { link: Craft.getCpUrl('plugin-store/freeform') }
    +                  )
                     ),
                   }}
                 />
    
  • packages/client/src/app/pages/import-export/common/preview/integrations/integrations.tsx+6 1 modified
    @@ -2,6 +2,7 @@ import React from 'react';
     import { Checkbox } from '@components/elements/checkbox/checkbox';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import type { Integration } from '../../../import/import.types';
     import {
    @@ -72,7 +73,11 @@ export const PreviewIntegrations: React.FC<Props> = ({
                   </BlockItem>
                   <Spacer $dash />
                   {!!integration.icon && (
    -                <Icon dangerouslySetInnerHTML={{ __html: integration.icon }} />
    +                <Icon
    +                  dangerouslySetInnerHTML={{
    +                    __html: sanitize(integration.icon),
    +                  }}
    +                />
                   )}
                   {!integration.icon && <Icon className="fa-duotone fa-gear" />}
                   <Label $light htmlFor={`integration-${integration.uid}`}>
    
  • packages/client/src/app/pages/import-export/common/preview/preview.tsx+2 1 modified
    @@ -2,6 +2,7 @@ import React from 'react';
     import { indexedColumn } from '@ff-client/utils/arrays';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { isAllOptionsSelected } from '../../export/export.operations';
     import {
    @@ -113,7 +114,7 @@ export const Preview: React.FC<Props> = ({
                   return (
                     <Icon
                       dangerouslySetInnerHTML={{
    -                    __html: integration.icon,
    +                    __html: sanitize(integration.icon),
                       }}
                     />
                   );
    
  • packages/client/src/app/pages/integrations/editor/readme/readme.tsx+5 2 modified
    @@ -1,6 +1,7 @@
     import type { FC } from 'react';
     import React from 'react';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     import { marked } from 'marked';
     
     import { Content, Instructions, MarkdownWrapper } from './readme.styles';
    @@ -13,7 +14,7 @@ type Props = {
     };
     
     export const Readme: FC<Props> = ({ active, content }) => {
    -  const parsedContent = marked.parse(content, { gfm: true });
    +  const parsedContent = marked.parse(content, { gfm: true, async: false });
     
       if (!content) {
         return <MarkdownWrapper />;
    @@ -22,7 +23,9 @@ export const Readme: FC<Props> = ({ active, content }) => {
       return (
         <MarkdownWrapper>
           <Instructions className={classes('markdown-body', active && 'active')}>
    -        <Content dangerouslySetInnerHTML={{ __html: parsedContent }} />
    +        <Content
    +          dangerouslySetInnerHTML={{ __html: sanitize(parsedContent) }}
    +        />
           </Instructions>
         </MarkdownWrapper>
       );
    
  • packages/client/src/app/pages/integrations/editor/titlebar/form-monitor/titlebar.modal.tsx+9 4 modified
    @@ -8,6 +8,7 @@ import { Modal } from '@ff-client/app/components/modals/modal';
     import { FormWrapper } from '@ff-client/app/pages/forms/edit/builder/tabs/form-monitor/form-monitor.action.modal.styles';
     import classes from '@ff-client/utils/classes';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     type Props = {
       onClose: () => void;
    @@ -51,8 +52,10 @@ export const DisableMonitoringModal: React.FC<Props> = ({
               </div>
               <div
                 dangerouslySetInnerHTML={{
    -              __html: translate(
    -                'To disable monitoring, please type <strong>CONFIRM</strong> in the box below:'
    +              __html: sanitize(
    +                translate(
    +                  'To disable monitoring, please type <strong>CONFIRM</strong> in the box below:'
    +                )
                   ),
                 }}
               />
    @@ -119,8 +122,10 @@ export const DisableAndDeleteMonitoringModal: React.FC<Props> = ({
               </div>
               <div
                 dangerouslySetInnerHTML={{
    -              __html: translate(
    -                'To disable monitoring and delete all data, please type <strong>CONFIRM</strong> in the box below:'
    +              __html: sanitize(
    +                translate(
    +                  'To disable monitoring and delete all data, please type <strong>CONFIRM</strong> in the box below:'
    +                )
                   ),
                 }}
               />
    
  • packages/client/src/app/pages/integrations/editor/titlebar/titlebar.tsx+6 1 modified
    @@ -8,6 +8,7 @@ import { notifications } from '@ff-client/utils/notifications';
     import translate from '@ff-client/utils/translations';
     import { useQueryClient } from '@tanstack/react-query';
     import axios from 'axios';
    +import { sanitize } from 'dompurify';
     
     import type { AuthState, Integration } from '../../integration.types';
     import { Readme } from '../readme/readme';
    @@ -93,7 +94,11 @@ export const Titlebar: FC<Props> = ({ integration }) => {
       return (
         <>
           <Title>
    -        <Icon dangerouslySetInnerHTML={{ __html: integration.type.iconSvg }} />
    +        <Icon
    +          dangerouslySetInnerHTML={{
    +            __html: sanitize(integration.type.iconSvg),
    +          }}
    +        />
             <span>{integration.name || integration.type.name}</span>
     
             {integration.type.version && (
    
  • packages/client/src/app/pages/integrations/sidebar/sidebar.tsx+2 1 modified
    @@ -3,6 +3,7 @@ import { NavLink, useLocation } from 'react-router-dom';
     import { Search } from '@components/search/search';
     import config from '@config/freeform/freeform.config';
     import classes from '@ff-client/utils/classes';
    +import { sanitize } from 'dompurify';
     
     import { useIntegrationNavigation } from './sidebar.queries';
     import {
    @@ -94,7 +95,7 @@ export const Sidebar: React.FC = () => {
                           {entry.type.iconSvg && (
                             <Icon
                               dangerouslySetInnerHTML={{
    -                            __html: entry.type.iconSvg,
    +                            __html: sanitize(entry.type.iconSvg),
                               }}
                             />
                           )}
    
  • packages/client/src/app/pages/limited-users/limited-users.sidebar.tsx+4 1 modified
    @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
     import { generateUrl } from '@ff-client/utils/urls';
     import { useQuery } from '@tanstack/react-query';
     import axios from 'axios';
    +import { sanitize } from 'dompurify';
     
     type Item = {
       title?: string;
    @@ -38,7 +39,9 @@ export const SettingsSidebar: React.FC = () => {
                         {key !== 'limited-users' && (
                           <a
                             href={generateUrl(`settings/${key}`)}
    -                        dangerouslySetInnerHTML={{ __html: item.title }}
    +                        dangerouslySetInnerHTML={{
    +                          __html: sanitize(item.title),
    +                        }}
                           />
                         )}
                       </li>
    
  • packages/client/src/app/pages/surveys/results/result-list/block/block.tsx+9 4 modified
    @@ -1,6 +1,7 @@
     import React, { useEffect, useRef, useState } from 'react';
     import { useFieldType } from '@ff-client/queries/field-types';
     import translate from '@ff-client/utils/translations';
    +import { sanitize } from 'dompurify';
     
     import { useQuerySurveyPreferences } from '../../results.queries';
     import type { Result } from '../../results.types';
    @@ -92,9 +93,11 @@ export const Block: React.FC<Props> = ({
             --{' '}
             <span
               dangerouslySetInnerHTML={{
    -            __html: translate('Question <b>{index}</b> Hidden', {
    -              index: bulletin,
    -            }),
    +            __html: sanitize(
    +              translate('Question <b>{index}</b> Hidden', {
    +                index: bulletin,
    +              })
    +            ),
               }}
             ></span>{' '}
             --
    @@ -110,7 +113,9 @@ export const Block: React.FC<Props> = ({
           <Label>
             <Heading>
               {fieldType && (
    -            <span dangerouslySetInnerHTML={{ __html: fieldType.icon }} />
    +            <span
    +              dangerouslySetInnerHTML={{ __html: sanitize(fieldType.icon) }}
    +            />
               )}
               {field.label}
             </Heading>
    
  • packages/client/src/styles/field-cells/cards.ts+8 0 added
    @@ -0,0 +1,8 @@
    +import { css } from 'styled-components';
    +
    +export const CardCell = css`
    +  .options-one-line {
    +    display: inline-block;
    +    margin-right: 10px;
    +  }
    +`;
    
  • packages/client/src/styles/field-cells/rating.ts+26 0 added
    @@ -0,0 +1,26 @@
    +import { css } from 'styled-components';
    +
    +export const RatingCell = css`
    +  .ff-rating {
    +    display: flex;
    +    justify-content: flex-start;
    +    flex-wrap: wrap;
    +
    +    > span {
    +      display: block;
    +      cursor: pointer;
    +
    +      font-size: 200%;
    +      font-weight: 100;
    +      font-family: sans-serif;
    +
    +      &:after {
    +        content: '★ ';
    +      }
    +
    +      &:last-child {
    +        margin: 0 0 5px;
    +      }
    +    }
    +  }
    +`;
    
  • packages/client/src/styles/field-cells/stripe.ts+74 0 added
    @@ -0,0 +1,74 @@
    +import { css } from 'styled-components';
    +
    +export const StripeCell = css`
    +  .stripe-demo {
    +    display: flex;
    +    flex-direction: column;
    +    gap: 10px;
    +
    +    > ul {
    +      display: flex;
    +      gap: 10px;
    +      justify-content: space-between;
    +      align-items: stretch;
    +
    +      > li {
    +        flex: 1;
    +        padding: 0.75rem;
    +
    +        border: 1px solid #e6e6e6;
    +        border-radius: 5px;
    +        background-color: white;
    +
    +        &.selected {
    +          border-color: #0570de;
    +          fill: #0570de;
    +          color: #0570de;
    +
    +          box-shadow:
    +            0px 1px 1px rgba(0, 0, 0, 0.03),
    +            0px 3px 6px rgba(0, 0, 0, 0.02),
    +            0 0 0 1px #0570de;
    +        }
    +
    +        &:not(.selected) {
    +          filter: blur(3px);
    +        }
    +
    +        .icon-container {
    +          display: block;
    +
    +          & svg,
    +          & img {
    +            height: 1.2em;
    +          }
    +        }
    +      }
    +    }
    +
    +    > div {
    +      display: grid;
    +      gap: 10px;
    +      grid-template-columns: 2fr 1fr 1fr;
    +      grid-template-areas:
    +        'cc-number expiry cvc'
    +        'country country country';
    +
    +      .cc-number {
    +        grid-area: cc-number;
    +      }
    +
    +      .expiry {
    +        grid-area: expiry;
    +      }
    +
    +      .cvc {
    +        grid-area: cvc;
    +      }
    +
    +      .country {
    +        grid-area: country;
    +      }
    +    }
    +  }
    +`;
    
  • packages/client/src/styles/field-cells/table.ts+145 0 added
    @@ -0,0 +1,145 @@
    +import { css } from 'styled-components';
    +
    +export const TableCell = css`
    +  .table-cell-preview {
    +    width: 100%;
    +
    +    margin: 0;
    +    border-spacing: 0;
    +    border-collapse: separate;
    +
    +    & th,
    +    & td {
    +      width: auto;
    +    }
    +
    +    & td {
    +      padding: 0 !important;
    +
    +      &.string-cell,
    +      &.text-cell {
    +        padding: 6px 10px !important;
    +      }
    +
    +      &.select-cell {
    +        padding: 4px 10px !important;
    +        text-align: center !important;
    +
    +        & .select {
    +          width: 100% !important;
    +        }
    +      }
    +
    +      &.checkbox-cell {
    +        padding: 6px 10px !important;
    +        text-align: center !important;
    +
    +        & .checkbox-label {
    +          display: flex;
    +          align-items: center;
    +          justify-content: center;
    +          padding: 1px 0 !important;
    +
    +          & label {
    +            position: relative !important;
    +          }
    +        }
    +      }
    +    }
    +
    +    &.columns-5 {
    +      & td,
    +      & th {
    +        width: 20% !important;
    +      }
    +    }
    +
    +    &.columns-4 {
    +      & td,
    +      & th {
    +        width: 25% !important;
    +      }
    +    }
    +
    +    &.columns-3 {
    +      & td,
    +      & th {
    +        width: 33.333333% !important;
    +      }
    +    }
    +
    +    &.columns-2 {
    +      & td,
    +      & th {
    +        width: 50% !important;
    +      }
    +    }
    +
    +    & thead {
    +      & tr {
    +        & th {
    +          border-left: 0 !important;
    +          border-right: 0 !important;
    +          color: #596673 !important;
    +          font-weight: 400 !important;
    +          padding: 6px 10px !important;
    +          background-color: #f3f7fc !important;
    +          border-top: 1px solid rgba(96, 125, 159, 0.25) !important;
    +          border-bottom: 1px solid rgba(51, 64, 77, 0.1) !important;
    +        }
    +
    +        & th:first-child {
    +          border-top-left-radius: 5px !important;
    +          border-bottom-left-radius: 0 !important;
    +          border-left: 1px solid rgba(96, 125, 159, 0.25) !important;
    +        }
    +
    +        & th:last-child {
    +          border-top-right-radius: 5px !important;
    +          border-bottom-right-radius: 0 !important;
    +          border-right: 1px solid rgba(96, 125, 159, 0.25) !important;
    +        }
    +      }
    +    }
    +
    +    & tbody {
    +      & tr {
    +        & td {
    +          padding: 0 !important;
    +          border-top: 0 !important;
    +          border-left: 0 !important;
    +          border-radius: 0 !important;
    +          background-color: white !important;
    +          border-right: 1px solid rgba(51, 64, 77, 0.1) !important;
    +          border-bottom: 1px solid rgba(51, 64, 77, 0.1) !important;
    +
    +          &:hover {
    +            background-color: white !important;
    +          }
    +        }
    +
    +        & td:first-child {
    +          border-left: 1px solid rgba(96, 125, 159, 0.25) !important;
    +        }
    +
    +        & td:last-child {
    +          border-right: 1px solid rgba(96, 125, 159, 0.25) !important;
    +        }
    +      }
    +
    +      & tr:last-child {
    +        & td {
    +          border-bottom: 1px solid rgba(96, 125, 159, 0.25) !important;
    +        }
    +
    +        & td:first-child {
    +          border-bottom-left-radius: 5px !important;
    +        }
    +
    +        & td:last-child {
    +          border-bottom-right-radius: 5px !important;
    +        }
    +      }
    +    }
    +  }
    +`;
    
  • packages/plugin/src/Fields/Implementations/PreviewTemplates/cards.ejs+0 7 modified
    @@ -1,10 +1,3 @@
    -<style>
    -    .options-one-line {
    -        display: inline-block;
    -        margin-right: 10px;
    -    }
    -</style>
    -
     <ul class="ff-cards" style="--card-columns: <%= cardsPerRow %>;">
         <% layout.forEach(function(card) { %>
             <li class="ff-cards__card">
    
  • packages/plugin/src/Fields/Implementations/PreviewTemplates/checkboxes.ejs+0 7 modified
    @@ -1,10 +1,3 @@
    -<style>
    -    .options-one-line {
    -        display: inline-block;
    -        margin-right: 10px;
    -    }
    -</style>
    -
     <% generatedOptions.forEach(function(option) { %>
     
         <% if (oneLine) { %><span class="options-one-line"><% } else { %><div><% } %>
    
  • packages/plugin/src/Fields/Implementations/PreviewTemplates/radios.ejs+0 7 modified
    @@ -1,10 +1,3 @@
    -<style>
    -    .options-one-line {
    -        display: inline-block;
    -        margin-right: 10px;
    -    }
    -</style>
    -
     <% generatedOptions.forEach(function(option) { %>
     
         <% if (oneLine) { %><span class="options-one-line"><% } else { %><div><% } %>
    
  • packages/plugin/src/Fields/Implementations/PreviewTemplates/rating.ejs+1 26 modified
    @@ -2,32 +2,7 @@
        var namespace = handle + '-' + Date.now();
     %>
     
    -<style>
    -        .rating<%= namespace %> {
    -                display: flex;
    -                justify-content: flex-start;
    -                flex-wrap: wrap;
    -        }
    -
    -        .rating<%= namespace %> > span {
    -                display: block;
    -                cursor: pointer;
    -
    -                font-size: 200%;
    -                font-weight: 100;
    -                font-family: sans-serif;
    -        }
    -
    -        .rating<%= namespace %> > span:after {
    -                content: '★ ';
    -        }
    -
    -        .rating<%= namespace %> > span:last-child {
    -                margin: 0 0 5px;
    -        }
    -</style>
    -
    -<div class="rating<%= namespace %>">
    +<div class="ff-rating">
         <% for (var i = 1; i <= maxValue; i++ ) { %>
             <span style="color: <%= i <= 2 ? colorSelected : colorIdle %>;"></span>
         <% } %>
    
  • packages/plugin/src/Fields/Implementations/PreviewTemplates/table.ejs+1 145 modified
    @@ -2,151 +2,7 @@
         Table field has not been configured yet.
     <% } %>
     
    -<style>
    -    .preview {
    -        width: 100%;
    -
    -        margin: 0;
    -        border-spacing: 0;
    -        border-collapse: separate;
    -
    -        & th,
    -        & td {
    -            width: auto;
    -        }
    -
    -        & td {
    -            padding: 0 !important;
    -
    -            &.string-cell,
    -            &.text-cell {
    -                padding: 6px 10px !important;
    -            }
    -
    -            &.select-cell {
    -                padding: 4px 10px !important;
    -                text-align: center !important;
    -
    -                & .select {
    -                    width: 100% !important;
    -                }
    -            }
    -
    -            &.checkbox-cell {
    -                padding: 6px 10px !important;
    -                text-align: center !important;
    -
    -                & .checkbox-label {
    -                    display: flex;
    -                    align-items: center;
    -                    justify-content: center;
    -                    padding: 1px 0 !important;
    -
    -                    & label {
    -                        position: relative !important;
    -                    }
    -                }
    -            }
    -        }
    -
    -        &.columns-5 {
    -            & td,
    -            & th {
    -                width: 20% !important;
    -            }
    -        }
    -
    -        &.columns-4 {
    -            & td,
    -            & th {
    -                width: 25% !important;
    -            }
    -        }
    -
    -        &.columns-3 {
    -            & td,
    -            & th {
    -                width: 33.333333% !important;
    -            }
    -        }
    -
    -        &.columns-2 {
    -            & td,
    -            & th {
    -                width: 50% !important;
    -            }
    -        }
    -
    -        & thead {
    -            & tr {
    -                & th {
    -                    border-left: 0 !important;
    -                    border-right: 0 !important;
    -                    color: #596673 !important;
    -                    font-weight: 400 !important;
    -                    padding: 6px 10px !important;
    -                    background-color: #f3f7fc !important;
    -                    border-top: 1px solid rgba(96,125,159,0.25) !important;
    -                    border-bottom: 1px solid rgba(51,64,77,0.1) !important;
    -                }
    -
    -                & th:first-child {
    -                    border-top-left-radius: 5px !important;
    -                    border-bottom-left-radius: 0 !important;
    -                    border-left: 1px solid rgba(96,125,159,0.25) !important;
    -                }
    -
    -                & th:last-child {
    -                    border-top-right-radius: 5px !important;
    -                    border-bottom-right-radius: 0 !important;
    -                    border-right: 1px solid rgba(96,125,159,0.25) !important;
    -                }
    -            }
    -        }
    -
    -        & tbody {
    -            & tr {
    -                & td {
    -                    padding: 0 !important;
    -                    border-top: 0 !important;
    -                    border-left: 0 !important;
    -                    border-radius: 0 !important;
    -                    background-color: white !important;
    -                    border-right: 1px solid rgba(51,64,77,0.1) !important;
    -                    border-bottom: 1px solid rgba(51,64,77,0.1) !important;
    -
    -                    &:hover {
    -                        background-color: white !important;
    -                    }
    -                }
    -
    -                & td:first-child {
    -                    border-left: 1px solid rgba(96,125,159,0.25) !important;
    -                }
    -
    -                & td:last-child {
    -                    border-right: 1px solid rgba(96,125,159,0.25) !important;
    -                }
    -            }
    -
    -            & tr:last-child {
    -                & td {
    -                    border-bottom: 1px solid rgba(96,125,159,0.25) !important;
    -                }
    -
    -                & td:first-child {
    -                    border-bottom-left-radius: 5px !important;
    -                }
    -
    -                & td:last-child {
    -                    border-bottom-right-radius: 5px !important;
    -                }
    -            }
    -        }
    -    }
    -</style>
    -
    -<table class="preview data fullwidth columns-<%= tableLayout.length %>">
    +<table class="table-cell-preview data fullwidth columns-<%= tableLayout.length %>">
         <thead>
             <tr>
                 <% tableLayout.forEach(function(column) { %>
    
  • packages/plugin/src/Integrations/PaymentGateways/Stripe/Templates/stripe-field-preview.ejs+0 70 modified
    @@ -1,73 +1,3 @@
    -<style>
    -    .stripe-demo {
    -        display: flex;
    -        flex-direction: column;
    -        gap: 10px;
    -
    -        > ul {
    -            display: flex;
    -            gap: 10px;
    -            justify-content: space-between;
    -            align-items: stretch;
    -
    -            > li {
    -                flex: 1;
    -                padding: 0.75rem;
    -
    -                border:1px solid #e6e6e6;
    -                border-radius: 5px;
    -                background-color: white;
    -
    -                &.selected {
    -                    border-color: #0570de;
    -                    fill: #0570de;
    -                    color: #0570de;
    -
    -                    box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.03),
    -                        0px 3px 6px rgba(0, 0, 0, 0.02),
    -                        0 0 0 1px #0570de
    -                }
    -
    -                &:not(.selected) {
    -                    filter: blur(3px);
    -                }
    -
    -                .icon-container {
    -                    display: block;
    -
    -                    & svg, & img {
    -                        height: 1.2em;
    -                    }
    -                }
    -            }
    -        }
    -
    -        > div {
    -            display: grid;
    -            gap: 10px;
    -            grid-template-columns: 2fr 1fr 1fr;
    -            grid-template-areas: 'cc-number expiry cvc'
    -                 'country country country';
    -
    -            .cc-number {
    -                grid-area: cc-number;
    -            }
    -
    -            .expiry {
    -                grid-area: expiry;
    -            }
    -
    -            .cvc {
    -                grid-area: cvc;
    -            }
    -
    -            .country {
    -                grid-area: country;
    -            }
    -        }
    -    }
    -</style>
    -
     <div class="stripe-demo">
         <ul>
             <li class="selected">
    
  • packages/plugin/src/Resources/js/client/client.js+1 1 modified
  • packages/plugin/src/Resources/js/client/vendor.js+1 1 modified
  • packages/plugin/src/Resources/js/client/vendor.js.LICENSE.txt+2 0 modified
    @@ -9,6 +9,8 @@
     * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
     */
     
    +/*! @license DOMPurify 3.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.1/LICENSE */
    +
     /*! Axios v1.13.2 Copyright (c) 2025 Matt Zabriskie and contributors */
     
     /*! decimal.js-light v2.5.1 https://github.com/MikeMcl/decimal.js-light/LICENCE */
    
  • packages/plugin/src/Resources/js/scripts/cp/code-pack/index.js+1 1 modified
    @@ -1 +1 @@
    -!function(){var a=$("#prefix"),e=$("#components-wrapper"),n=$("> div > ul.directory-structure",e),r=$(".btn.submit"),t=null;function o(){n.each(function(){var e=$(this);$("> li > span[data-name]",e).each(function(){$(this).html(a.val()+$(this).data("name"))})})}$(function(){a.on({keyup:function(){/[\\/]/gi.test(a.val())?(a.addClass("error"),r.addClass("disabled").prop("disabled",!0).prop("readonly",!0)):(a.removeClass("error"),r.removeClass("disabled").prop("disabled",!1).prop("readonly",!1)),clearTimeout(t),t=setTimeout(function(){o()},50)}}),o()})}();
    \ No newline at end of file
    +!function(){var e=$("#prefix"),a=$("#components-wrapper"),t=$("> div > ul.directory-structure",a),n=$(".btn.submit"),r=null;function o(){t.each(function(){var a=$(this);$("> li > span[data-name]",a).each(function(){$(this).text(e.val()+$(this).data("name"))})})}$(function(){e.on({keyup:function(){/[\\/]/gi.test(e.val())?(e.addClass("error"),n.addClass("disabled").prop("disabled",!0).prop("readonly",!0)):(e.removeClass("error"),n.removeClass("disabled").prop("disabled",!1).prop("readonly",!1)),clearTimeout(r),r=setTimeout(function(){o()},50)}}),o()})}();
    \ No newline at end of file
    
  • packages/plugin/src/Resources/js/scripts/front-end/plugin/freeform.js+1 1 modified
  • packages/scripts/package.json+1 1 modified
    @@ -38,7 +38,7 @@
         "@paypal/paypal-js": "^8.4.2",
         "@stripe/stripe-js": "^2.1.9",
         "clsx": "^2.0.0",
    -    "expression-language": "^1.1.4",
    +    "expression-language": "^2.5.1",
         "filesize": "^10.0.12",
         "locutus": "^2.0.16"
       }
    
  • packages/scripts/src/components/cp/code-pack/index.js+1 1 modified
    @@ -30,7 +30,7 @@ function updateFilePrefixes() {
       firstFileLists.each(function () {
         const $fileList = $(this);
         $('> li > span[data-name]', $fileList).each(function () {
    -      $(this).html($prefix.val() + $(this).data('name'));
    +      $(this).text($prefix.val() + $(this).data('name'));
         });
       });
     }
    
  • packages/scripts/src/components/front-end/fields/calculation.ts+5 1 modified
    @@ -41,7 +41,11 @@ const extractValue = (element: HTMLInputElement | HTMLSelectElement): string | n
         return false;
       }
     
    -  return isNaN(Number(value)) ? value : Number(value);
    +  if (isNaN(Number(value))) {
    +    return value;
    +  }
    +
    +  return Number(value);
     };
     
     const attachCalculations = (input: HTMLInputElement) => {
    
  • packages/scripts/src/lib/plugin/helpers/html.ts+28 2 modified
    @@ -20,7 +20,12 @@ export const createScript: ScriptCreator = (src, options = {}) => {
     
       if (!scriptCache.has(key)) {
         const script = document.createElement('script');
    -    script.src = src;
    +    const safeSrc = normalizeUrl(src);
    +    if (!safeSrc) {
    +      throw new Error(`Unsafe script URL: ${src}`);
    +    }
    +
    +    script.src = safeSrc;
         script.async = async ?? false;
         script.defer = defer ?? false;
     
    @@ -66,8 +71,14 @@ export const createLink: LinkCreator = (href, options = {}) => {
     
       if (!linkCache.has(key)) {
         const link = document.createElement('link');
    +
    +    const safeHref = normalizeUrl(href);
    +    if (!safeHref) {
    +      throw new Error(`Unsafe stylesheet URL: ${href}`);
    +    }
    +
         link.rel = 'stylesheet';
    -    link.href = href;
    +    link.href = safeHref;
     
         link.addEventListener('load', () => {
           if (onLoad) {
    @@ -95,3 +106,18 @@ export const createLink: LinkCreator = (href, options = {}) => {
     
       return linkCache.get(key) as HTMLLinkElement;
     };
    +
    +const normalizeUrl = (raw: string): string => {
    +  try {
    +    const url = new URL(raw, window.location.href);
    +    if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    +      return '';
    +    }
    +
    +    return url.toString();
    +  } catch {
    +    // Invalid URL
    +  }
    +
    +  return '';
    +};
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.