Skip to content

Commit 1cf6c40

Browse files
author
mhirdes
committed
[SECUTRIY] escape JSON-LD output to prevent XSS #2025032510000016
1 parent cc658d5 commit 1cf6c40

File tree

4 files changed

+73
-15
lines changed

4 files changed

+73
-15
lines changed

Classes/Evaluation/TCA/JsonLdEvaluator.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,57 @@
22

33
namespace Clickstorm\CsSeo\Evaluation\TCA;
44

5+
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
6+
57
class JsonLdEvaluator extends AbstractEvaluator
68
{
9+
private static bool $alreadyValidated = false;
10+
711
/**
812
* Server-side validation/evaluation on saving the record
913
*
1014
*/
1115
public function evaluateFieldValue(string $value, string $is_in, bool &$set): string
1216
{
13-
if ($value && !isset($_REQUEST['tx_csseo_json_ld_eval_done'])) {
14-
$value = trim(preg_replace('#<script(.*?)>|</script>#is', '', $value));
15-
if ($value && json_decode($value, true) === null) {
16-
$this->addFlashMessage(
17-
'LLL:EXT:cs_seo/Resources/Private/Language/locallang_db.xlf:evaluation.tca.json_ld.invalid_json'
18-
);
19-
}
17+
if (self::$alreadyValidated || empty($value)) {
18+
return $value;
19+
}
20+
21+
self::$alreadyValidated = true;
22+
23+
// Remove surrounding <script> tag if present
24+
$value = $this->stripScriptWrapper($value);
25+
26+
$decoded = json_decode($value, true);
27+
28+
if (json_last_error() !== JSON_ERROR_NONE) {
29+
$this->addFlashMessage(
30+
'LLL:EXT:cs_seo/Resources/Private/Language/locallang_db.xlf:evaluation.tca.json_ld.invalid_json'
31+
);
32+
33+
$invalidComment = LocalizationUtility::translate(
34+
'error.invalid_json_comment',
35+
'cs_seo'
36+
) ?? '/* INVALID JSON */';
37+
38+
return $invalidComment . "\n" . $value;
2039
}
2140

22-
$_REQUEST['tx_csseo_json_ld_eval_done'] = true;
41+
return $value;
42+
}
43+
44+
/**
45+
* Remove <script type="application/ld+json"> wrapper from pasted code
46+
*/
47+
protected function stripScriptWrapper(string $value): string
48+
{
49+
// Normalize whitespace and remove script wrapper
50+
$value = trim($value);
51+
52+
// Match and extract the contents inside <script type="application/ld+json">...</script>
53+
if (preg_match('#<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>#is', $value, $matches)) {
54+
return trim($matches[1]);
55+
}
2356

2457
return $value;
2558
}

Classes/UserFunc/StructuredData.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,36 @@ public function getJsonLdOfPageOrRecord(string $content, array $conf): string
5555

5656
// overwrite json ld with record metadata
5757
if ($metaData) {
58-
$jsonLd = $metaData['json_ld'] ?? null;
58+
$jsonLd = $metaData['json_ld'] ?? '';
5959
}
6060

61-
return $jsonLd ? $this->wrapWithLd($jsonLd) : '';
61+
// if empty return nothing
62+
if (empty($jsonLd)) {
63+
return '';
64+
}
65+
66+
// Try to decode the JSON string to ensure it's valid
67+
$tempJson = json_decode($jsonLd, true);
68+
69+
// Check if decoding was successful
70+
if (json_last_error() === JSON_ERROR_NONE) {
71+
72+
// Re-encode the array to sanitize any unexpected content
73+
$safeJson = json_encode($tempJson, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
74+
75+
return $this->wrapWithLd($safeJson);
76+
}
77+
78+
return '<!-- Invalid JSON-LD -->';
6279
}
6380

6481
/**
6582
* Wraps $content with Json declaration
6683
*/
6784
protected function wrapWithLd(string $content): string
6885
{
69-
return '<script type="application/ld+json">' . $content . '</script>';
86+
// Escape special characters to prevent breaking out of the script tag
87+
return '<script type="application/ld+json">' . htmlspecialchars($content, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</script>';
7088
}
7189

7290
public function getSiteSearch(string $content, array $conf): string
@@ -93,7 +111,7 @@ public function getSiteSearch(string $content, array $conf): string
93111
],
94112
];
95113

96-
return $this->wrapWithLd(json_encode($siteSearch));
114+
return $this->wrapWithLd(json_encode($siteSearch, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
97115
}
98116

99117
/**
@@ -165,6 +183,6 @@ public function getBreadcrumb(string $conf, array $content): string
165183
'itemListElement' => $breadcrumbItems,
166184
];
167185

168-
return $this->wrapWithLd(json_encode($structuredBreadcrumb));
186+
return $this->wrapWithLd(json_encode($structuredBreadcrumb, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
169187
}
170188
}

Resources/Private/Language/de.locallang.xlf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
3-
<file source-language="en" target-language="de" datatype="plaintext" original="EXT:cs_seo/Resources/Private/Language/locallang.xlf" date="2024-05-17T07:49:36Z" product-name="cs_seo">
3+
<file source-language="en" target-language="de" datatype="plaintext" original="EXT:cs_seo/Resources/Private/Language/locallang.xlf" date="2025-04-15T10:00:55Z" product-name="cs_seo">
44
<header/>
55
<body>
66
<trans-unit id="date.format" resname="date.format">
@@ -19,6 +19,10 @@
1919
<source>The TypoScript of the current page could not be loaded.</source>
2020
<target>Das TypoScript dieser Seite konnte nicht geladen werden.</target>
2121
</trans-unit>
22+
<trans-unit id="error.invalid_json_comment" resname="error.invalid_json_comment">
23+
<source>INVALID JSON!</source>
24+
<target>UNGÜLTIGES JSON!</target>
25+
</trans-unit>
2226
<trans-unit id="evaluation.description" resname="evaluation.description">
2327
<source>Meta Description</source>
2428
<target>Seitenbeschreibung</target>

Resources/Private/Language/locallang.xlf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
3-
<file source-language="en" datatype="plaintext" original="EXT:cs_seo/Resources/Private/Language/locallang.xlf" date="2024-05-17T07:49:36Z" product-name="cs_seo">
3+
<file source-language="en" datatype="plaintext" original="EXT:cs_seo/Resources/Private/Language/locallang.xlf" date="2025-04-15T10:00:55Z" product-name="cs_seo">
44
<header/>
55
<body>
66
<trans-unit id="date.format" resname="date.format">
@@ -15,6 +15,9 @@
1515
<trans-unit id="error.no_tsfe" resname="error.no_tsfe">
1616
<source>The TypoScript of the current page could not be loaded.</source>
1717
</trans-unit>
18+
<trans-unit id="error.invalid_json_comment" resname="error.invalid_json_comment">
19+
<source>INVALID JSON!</source>
20+
</trans-unit>
1821
<trans-unit id="evaluation.description" resname="evaluation.description">
1922
<source>Meta Description</source>
2023
</trans-unit>

0 commit comments

Comments
 (0)