Properly HTML escape fields for label template placeholders

Fixes issue #1446
This commit is contained in:
Jan Böhmer 2026-07-19 14:03:14 +02:00
parent 5054642d41
commit dcc431d28c
10 changed files with 43 additions and 42 deletions

View file

@ -67,8 +67,8 @@ class BarcodeHelper
{ {
$svg = $this->barcodeAsSVG($content, $type); $svg = $this->barcodeAsSVG($content, $type);
$base64 = $this->dataUri($svg, 'image/svg+xml'); $base64 = $this->dataUri($svg, 'image/svg+xml');
$alt_text ??= $content; $alt_text ??= htmlspecialchars($content);
return '<img src="'.$base64.'" width="'.$width.'" style="min-height: 25px;" alt="'.$alt_text.'"/>'; return '<img src="'.$base64.'" width="'.$width.'" style="min-height: 25px;" alt="'.$alt_text.'"/>';
} }
@ -94,4 +94,4 @@ class BarcodeHelper
return $repr; return $repr;
} }
} }

View file

@ -44,9 +44,9 @@ namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractDBElement;
use App\Services\ElementTypeNameGenerator; use App\Services\ElementTypeNameGenerator;
final class AbstractDBElementProvider implements PlaceholderProviderInterface final readonly class AbstractDBElementProvider implements PlaceholderProviderInterface
{ {
public function __construct(private readonly ElementTypeNameGenerator $elementTypeNameGenerator) public function __construct(private ElementTypeNameGenerator $elementTypeNameGenerator)
{ {
} }
@ -54,7 +54,7 @@ final class AbstractDBElementProvider implements PlaceholderProviderInterface
{ {
if ($label_target instanceof AbstractDBElement) { if ($label_target instanceof AbstractDBElement) {
if ('[[TYPE]]' === $placeholder) { if ('[[TYPE]]' === $placeholder) {
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($label_target); return $this->elementTypeNameGenerator->typeLabel($label_target);
} }
if ('[[ID]]' === $placeholder) { if ('[[ID]]' === $placeholder) {

View file

@ -31,11 +31,11 @@ use App\Services\LabelSystem\LabelBarcodeGenerator;
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator; use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
use Com\Tecnick\Barcode\Exception; use Com\Tecnick\Barcode\Exception;
final class BarcodeProvider implements PlaceholderProviderInterface final readonly class BarcodeProvider implements PlaceholderProviderInterface
{ {
public function __construct(private readonly LabelBarcodeGenerator $barcodeGenerator, public function __construct(private LabelBarcodeGenerator $barcodeGenerator,
private readonly BarcodeContentGenerator $barcodeContentGenerator, private BarcodeContentGenerator $barcodeContentGenerator,
private readonly BarcodeHelper $barcodeHelper) private BarcodeHelper $barcodeHelper)
{ {
} }

View file

@ -53,11 +53,11 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
* Provides Placeholders for infos about global infos like Installation name or datetimes. * Provides Placeholders for infos about global infos like Installation name or datetimes.
* @see \App\Tests\Services\LabelSystem\PlaceholderProviders\GlobalProvidersTest * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\GlobalProvidersTest
*/ */
final class GlobalProviders implements PlaceholderProviderInterface final readonly class GlobalProviders implements PlaceholderProviderInterface
{ {
public function __construct( public function __construct(
private readonly Security $security, private Security $security,
private readonly UrlGeneratorInterface $url_generator, private UrlGeneratorInterface $url_generator,
private CustomizationSettings $customizationSettings, private CustomizationSettings $customizationSettings,
) )
{ {
@ -66,13 +66,13 @@ final class GlobalProviders implements PlaceholderProviderInterface
public function replace(string $placeholder, object $label_target, array $options = []): ?string public function replace(string $placeholder, object $label_target, array $options = []): ?string
{ {
if ('[[INSTALL_NAME]]' === $placeholder) { if ('[[INSTALL_NAME]]' === $placeholder) {
return $this->customizationSettings->instanceName; return htmlspecialchars($this->customizationSettings->instanceName);
} }
$user = $this->security->getUser(); $user = $this->security->getUser();
if ('[[USERNAME]]' === $placeholder) { if ('[[USERNAME]]' === $placeholder) {
if ($user instanceof User) { if ($user instanceof User) {
return $user->getName(); return htmlspecialchars($user->getName());
} }
return 'anonymous'; return 'anonymous';
@ -80,7 +80,7 @@ final class GlobalProviders implements PlaceholderProviderInterface
if ('[[USERNAME_FULL]]' === $placeholder) { if ('[[USERNAME_FULL]]' === $placeholder) {
if ($user instanceof User) { if ($user instanceof User) {
return $user->getFullName(true); return htmlspecialchars($user->getFullName(true));
} }
return 'anonymous'; return 'anonymous';

View file

@ -51,7 +51,7 @@ final class NamedElementProvider implements PlaceholderProviderInterface
public function replace(string $placeholder, object $label_target, array $options = []): ?string public function replace(string $placeholder, object $label_target, array $options = []): ?string
{ {
if ($label_target instanceof NamedElementInterface && '[[NAME]]' === $placeholder) { if ($label_target instanceof NamedElementInterface && '[[NAME]]' === $placeholder) {
return $label_target->getName(); return htmlspecialchars($label_target->getName());
} }
return null; return null;

View file

@ -52,9 +52,9 @@ use Locale;
/** /**
* @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartLotProviderTest * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartLotProviderTest
*/ */
final class PartLotProvider implements PlaceholderProviderInterface final readonly class PartLotProvider implements PlaceholderProviderInterface
{ {
public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly AmountFormatter $amountFormatter) public function __construct(private LabelTextReplacer $labelTextReplacer, private AmountFormatter $amountFormatter)
{ {
} }
@ -66,11 +66,11 @@ final class PartLotProvider implements PlaceholderProviderInterface
} }
if ('[[LOT_NAME]]' === $placeholder) { if ('[[LOT_NAME]]' === $placeholder) {
return $label_target->getName(); return htmlspecialchars($label_target->getName());
} }
if ('[[LOT_COMMENT]]' === $placeholder) { if ('[[LOT_COMMENT]]' === $placeholder) {
return $label_target->getComment(); return htmlspecialchars($label_target->getComment());
} }
if ('[[EXPIRATION_DATE]]' === $placeholder) { if ('[[EXPIRATION_DATE]]' === $placeholder) {
@ -95,19 +95,19 @@ final class PartLotProvider implements PlaceholderProviderInterface
} }
if ('[[LOCATION]]' === $placeholder) { if ('[[LOCATION]]' === $placeholder) {
return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getName() : ''; return $label_target->getStorageLocation() instanceof StorageLocation ? htmlspecialchars($label_target->getStorageLocation()->getName()) : '';
} }
if ('[[LOCATION_FULL]]' === $placeholder) { if ('[[LOCATION_FULL]]' === $placeholder) {
return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getFullPath() : ''; return $label_target->getStorageLocation() instanceof StorageLocation ? htmlspecialchars($label_target->getStorageLocation()->getFullPath()) : '';
} }
if ('[[OWNER]]' === $placeholder) { if ('[[OWNER]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : ''; return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getFullName()) : '';
} }
if ('[[OWNER_USERNAME]]' === $placeholder) { if ('[[OWNER_USERNAME]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : ''; return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getUsername()) : '';
} }
return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart()); return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart());

View file

@ -54,11 +54,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartProviderTest * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartProviderTest
*/ */
final class PartProvider implements PlaceholderProviderInterface final readonly class PartProvider implements PlaceholderProviderInterface
{ {
private readonly MarkdownConverter $inlineConverter; private MarkdownConverter $inlineConverter;
public function __construct(private readonly SIFormatter $siFormatter, private readonly TranslatorInterface $translator) public function __construct(private SIFormatter $siFormatter, private TranslatorInterface $translator)
{ {
$environment = new Environment(); $environment = new Environment();
$environment->addExtension(new InlinesOnlyExtension()); $environment->addExtension(new InlinesOnlyExtension());
@ -72,27 +72,27 @@ final class PartProvider implements PlaceholderProviderInterface
} }
if ('[[CATEGORY]]' === $placeholder) { if ('[[CATEGORY]]' === $placeholder) {
return $part->getCategory() instanceof Category ? $part->getCategory()->getName() : ''; return $part->getCategory() instanceof Category ? htmlspecialchars($part->getCategory()->getName()) : '';
} }
if ('[[CATEGORY_FULL]]' === $placeholder) { if ('[[CATEGORY_FULL]]' === $placeholder) {
return $part->getCategory() instanceof Category ? $part->getCategory()->getFullPath() : ''; return $part->getCategory() instanceof Category ? htmlspecialchars($part->getCategory()->getFullPath()) : '';
} }
if ('[[MANUFACTURER]]' === $placeholder) { if ('[[MANUFACTURER]]' === $placeholder) {
return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getName() : ''; return $part->getManufacturer() instanceof Manufacturer ? htmlspecialchars($part->getManufacturer()->getName()) : '';
} }
if ('[[MANUFACTURER_FULL]]' === $placeholder) { if ('[[MANUFACTURER_FULL]]' === $placeholder) {
return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getFullPath() : ''; return $part->getManufacturer() instanceof Manufacturer ? htmlspecialchars($part->getManufacturer()->getFullPath()) : '';
} }
if ('[[FOOTPRINT]]' === $placeholder) { if ('[[FOOTPRINT]]' === $placeholder) {
return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : ''; return $part->getFootprint() instanceof Footprint ? htmlspecialchars($part->getFootprint()->getName()) : '';
} }
if ('[[FOOTPRINT_FULL]]' === $placeholder) { if ('[[FOOTPRINT_FULL]]' === $placeholder) {
return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getFullPath() : ''; return $part->getFootprint() instanceof Footprint ? htmlspecialchars($part->getFootprint()->getFullPath()) : '';
} }
if ('[[MASS]]' === $placeholder) { if ('[[MASS]]' === $placeholder) {
@ -100,15 +100,15 @@ final class PartProvider implements PlaceholderProviderInterface
} }
if ('[[MPN]]' === $placeholder) { if ('[[MPN]]' === $placeholder) {
return $part->getManufacturerProductNumber(); return htmlspecialchars($part->getManufacturerProductNumber());
} }
if ('[[IPN]]' === $placeholder) { if ('[[IPN]]' === $placeholder) {
return $part->getIpn() ?? ''; return $part->getIpn() ? htmlspecialchars($part->getIpn()) : '';
} }
if ('[[TAGS]]' === $placeholder) { if ('[[TAGS]]' === $placeholder) {
return $part->getTags(); return htmlspecialchars($part->getTags());
} }
if ('[[M_STATUS]]' === $placeholder) { if ('[[M_STATUS]]' === $placeholder) {

View file

@ -46,6 +46,7 @@ interface PlaceholderProviderInterface
/** /**
* Determines the real value of this placeholder. * Determines the real value of this placeholder.
* If the placeholder is not supported, null must be returned. * If the placeholder is not supported, null must be returned.
* The placeholder provider has to handle HTML escaping, as the output of this function will be used directly in the label template.
* *
* @param string $placeholder The placeholder (e.g. "%%PLACEHOLDER%%") that should be replaced * @param string $placeholder The placeholder (e.g. "%%PLACEHOLDER%%") that should be replaced
* @param object $label_target The object that is targeted by the label * @param object $label_target The object that is targeted by the label

View file

@ -31,11 +31,11 @@ class StorelocationProvider implements PlaceholderProviderInterface
{ {
if ($label_target instanceof StorageLocation) { if ($label_target instanceof StorageLocation) {
if ('[[OWNER]]' === $placeholder) { if ('[[OWNER]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : ''; return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getFullName()) : '';
} }
if ('[[OWNER_USERNAME]]' === $placeholder) { if ('[[OWNER_USERNAME]]' === $placeholder) {
return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : ''; return $label_target->getOwner() instanceof User ? htmlspecialchars($label_target->getOwner()->getUsername()) : '';
} }
} }

View file

@ -55,13 +55,13 @@ final class StructuralDBElementProvider implements PlaceholderProviderInterface
return strip_tags((string) $label_target->getComment()); return strip_tags((string) $label_target->getComment());
} }
if ('[[FULL_PATH]]' === $placeholder) { if ('[[FULL_PATH]]' === $placeholder) {
return $label_target->getFullPath(); return htmlspecialchars($label_target->getFullPath());
} }
if ('[[PARENT]]' === $placeholder) { if ('[[PARENT]]' === $placeholder) {
return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getName() : ''; return $label_target->getParent() instanceof AbstractStructuralDBElement ? htmlspecialchars($label_target->getParent()->getName()) : '';
} }
if ('[[PARENT_FULL_PATH]]' === $placeholder) { if ('[[PARENT_FULL_PATH]]' === $placeholder) {
return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getFullPath() : ''; return $label_target->getParent() instanceof AbstractStructuralDBElement ? htmlspecialchars($label_target->getParent()->getFullPath()) : '';
} }
} }