Solspace Freeform plugin affected by Stored Cross-Site Scripting (XSS) in Freeform Craft Plugin CP UI (builder/integrations)
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.
| Package | Affected versions | Patched versions |
|---|---|---|
solspace/craft-freeformPackagist | >= 5.0.0, < 5.14.7 | 5.14.7 |
Affected products
1- Range: >= 5.0.0, < 5.14.7
Patches
1b9adad6cdf1efix(SFT-2567): resolve all code scanning alerts (#2342)
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 modifiedpackages/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 modifiedpackages/plugin/src/Resources/js/client/vendor.js+1 −1 modifiedpackages/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 modifiedpackages/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- github.com/advisories/GHSA-jp3q-wwp3-pwv9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-26188ghsaADVISORY
- github.com/solspace/craft-freeform/commit/b9adad6cdf1eba5400aae8b1ae39bd7d4d33af5eghsax_refsource_MISCWEB
- github.com/solspace/craft-freeform/releases/tag/v5.14.7ghsax_refsource_MISCWEB
- github.com/solspace/craft-freeform/security/advisories/GHSA-jp3q-wwp3-pwv9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.