diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index 9132d6c..6f5d88b 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -62,6 +62,11 @@ class Plugin extends Model
$model->current_image = null;
}
});
+
+ // Sanitize configuration template on save
+ static::saving(function ($model): void {
+ $model->sanitizeTemplate();
+ });
}
public function user()
@@ -69,6 +74,25 @@ class Plugin extends Model
return $this->belongsTo(User::class);
}
+ // sanitize configuration template descriptions and help texts (since they allow HTML rendering)
+ protected function sanitizeTemplate(): void
+ {
+ $template = $this->configuration_template;
+
+ if (isset($template['custom_fields']) && is_array($template['custom_fields'])) {
+ foreach ($template['custom_fields'] as &$field) {
+ if (isset($field['description'])) {
+ $field['description'] = \Stevebauman\Purify\Facades\Purify::clean($field['description']);
+ }
+ if (isset($field['help_text'])) {
+ $field['help_text'] = \Stevebauman\Purify\Facades\Purify::clean($field['help_text']);
+ }
+ }
+
+ $this->configuration_template = $template;
+ }
+ }
+
public function hasMissingRequiredConfigurationFields(): bool
{
if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) {
diff --git a/composer.json b/composer.json
index f801679..0ced4da 100644
--- a/composer.json
+++ b/composer.json
@@ -26,6 +26,7 @@
"livewire/volt": "^1.7",
"om/icalparser": "^3.2",
"spatie/browsershot": "^5.0",
+ "stevebauman/purify": "^6.3",
"symfony/yaml": "^7.3",
"wnx/sidecar-browsershot": "^2.6"
},
diff --git a/composer.lock b/composer.lock
index b9e0495..9767a0d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "4d958d48655a5ad9e3de6b4a9fb52b0a",
+ "content-hash": "25c2a1a4a2f2594adefe25ddb6a072fb",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -814,6 +814,67 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
+ {
+ "name": "ezyang/htmlpurifier",
+ "version": "v4.19.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezyang/htmlpurifier.git",
+ "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
+ "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
+ },
+ "require-dev": {
+ "cerdic/css-tidy": "^1.7 || ^2.0",
+ "simpletest/simpletest": "dev-master"
+ },
+ "suggest": {
+ "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+ "ext-bcmath": "Used for unit conversion and imagecrash protection",
+ "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+ "ext-tidy": "Used for pretty-printing HTML"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/HTMLPurifier.composer.php"
+ ],
+ "psr-0": {
+ "HTMLPurifier": "library/"
+ },
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "description": "Standards compliant HTML filter written in PHP",
+ "homepage": "http://htmlpurifier.org/",
+ "keywords": [
+ "html"
+ ],
+ "support": {
+ "issues": "https://github.com/ezyang/htmlpurifier/issues",
+ "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
+ },
+ "time": "2025-10-17T16:34:55+00:00"
+ },
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
@@ -4947,6 +5008,72 @@
],
"time": "2025-01-13T13:04:43+00:00"
},
+ {
+ "name": "stevebauman/purify",
+ "version": "v6.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stevebauman/purify.git",
+ "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stevebauman/purify/zipball/3acb5e77904f420ce8aad8fa1c7f394e82daa500",
+ "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500",
+ "shasum": ""
+ },
+ "require": {
+ "ezyang/htmlpurifier": "^4.17",
+ "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
+ "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Purify": "Stevebauman\\Purify\\Facades\\Purify"
+ },
+ "providers": [
+ "Stevebauman\\Purify\\PurifyServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Stevebauman\\Purify\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Steve Bauman",
+ "email": "steven_bauman@outlook.com"
+ }
+ ],
+ "description": "An HTML Purifier / Sanitizer for Laravel",
+ "keywords": [
+ "Purifier",
+ "clean",
+ "cleaner",
+ "html",
+ "laravel",
+ "purification",
+ "purify"
+ ],
+ "support": {
+ "issues": "https://github.com/stevebauman/purify/issues",
+ "source": "https://github.com/stevebauman/purify/tree/v6.3.1"
+ },
+ "time": "2025-05-21T16:53:09+00:00"
+ },
{
"name": "symfony/clock",
"version": "v8.0.0",
diff --git a/resources/css/app.css b/resources/css/app.css
index 46b9ca1..30cb7a1 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -59,6 +59,10 @@
@apply !mb-0 !leading-tight;
}
+[data-flux-description] a {
+ @apply text-accent underline hover:opacity-80;
+}
+
input:focus[data-flux-control],
textarea:focus[data-flux-control],
select:focus[data-flux-control] {
diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php
index 4be96cc..e8ab799 100644
--- a/resources/views/livewire/plugins/recipe.blade.php
+++ b/resources/views/livewire/plugins/recipe.blade.php
@@ -264,7 +264,7 @@ new class extends Component {
$fieldKey = $field['keyname'];
if (isset($this->configuration[$fieldKey])) {
$value = $this->configuration[$fieldKey];
-
+
// For code fields, if the value is a JSON string and the original was an array, decode it
if ($field['field_type'] === 'code' && is_string($value)) {
$decoded = json_decode($value, true);
@@ -274,7 +274,7 @@ new class extends Component {
$value = $decoded;
}
}
-
+
$configurationValues[$fieldKey] = $value;
}
}
@@ -639,7 +639,14 @@ HTML;
@php
$fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
$rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? '');
-
+ # These are sanitized at PluginImportService when imported, safe to render HTML
+ $safeDescription = $field['description'] ?? '';
+ $safeHelp = $field['help_text'] ?? '';
+
+ //Important: Sanitize with Purify to prevent XSS attacks
+ // $safeDescription = Stevebauman\Purify\Facades\Purify::clean($field['description'] ?? '');
+ // $safeHelp = Stevebauman\Purify\Facades\Purify::clean($field['help_text'] ?? '');
+
// For code fields, if the value is an array, JSON encode it
if ($field['field_type'] === 'code' && is_array($rawValue)) {
$currentValue = json_encode($rawValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
@@ -657,176 +664,211 @@ HTML;
@endif
@if($field['field_type'] === 'string' || $field['field_type'] === 'url')
-