diff --git a/public/themes/material-blue/views/_partials/fixed-header.inc b/public/themes/material-blue/views/_partials/fixed-header.inc index 4b4bd9a82..b607e2819 100644 --- a/public/themes/material-blue/views/_partials/fixed-header.inc +++ b/public/themes/material-blue/views/_partials/fixed-header.inc @@ -34,7 +34,7 @@ use function SP\__; getData() as $dataName => $dataValue): ?> - + > getIcon()->getIcon(); ?> @@ -102,7 +102,7 @@ use function SP\__; getData() as $dataName => $dataValue): ?> - + > getIcon()->getIcon(); ?> getTitle()); ?> diff --git a/public/themes/material-blue/views/account/search-rows.inc b/public/themes/material-blue/views/account/search-rows.inc index 88334971b..a1fe8710e 100644 --- a/public/themes/material-blue/views/account/search-rows.inc +++ b/public/themes/material-blue/views/account/search-rows.inc @@ -421,8 +421,14 @@ $favoriteRouteOff = $_getvar('favoriteRouteOff'); data-parent-id="getParentId(); ?>" getData() as $dataName => $dataValue): - printf('data-%s="%s"', $dataName, $dataValue); + printf('data-%s="%s"', $dataName, $_e($dataValue)); endforeach; ?>> getIcon()->getIcon(); ?> @@ -459,7 +465,7 @@ $favoriteRouteOff = $_getvar('favoriteRouteOff'); echo $accountSearchData->getParentId(); ?>" getData() as $dataName => $dataValue): - printf('data-%s="%s"', $dataName, $dataValue); + printf('data-%s="%s"', $dataName, $_e($dataValue)); endforeach; ?>> getData() as $dataName => $dataValue - ): echo 'data-', $dataName, '=', '"', $dataValue, '"'; endforeach; ?>> + ): echo 'data-', $dataName, '=', '"', $_e($dataValue), '"'; endforeach; ?>> getIcon()->getIcon(); ?> @@ -90,7 +90,7 @@ $index = $index ?? 0; getData() as $dataName => $dataValue - ): echo 'data-', $dataName, '=', '"', $dataValue, '"'; endforeach; ?>> + ): echo 'data-', $dataName, '=', '"', $_e($dataValue), '"'; endforeach; ?>>
=""` out of + * `DataGridAction::getData()` / `getRuntimeData()`, where the value is a bare `$dataValue` — + * invisible to that rule, and unescaped. What reaches them today is routes and integer ids, so + * nothing was leaking; but nothing in the templates or in `DataGridActionBase` constrains it, + * and a single `addData()` call carrying a name, a login or a note would break straight out of + * the attribute. + * + * This is narrower than the rule above on purpose: it looks only at the loops whose attribute + * *name* is itself a variable, which are exactly the ones taking an arbitrary map from a + * caller. A fixed `data-item-id=""` is an id by construction and is left to + * the rule above. + */ + #[Test] + #[DataProvider('templates')] + public function aDataAttributeMapIsEscaped(string $template): void + { + $source = (string)file_get_contents($template); + + $unescaped = []; + + // printf('data-%s="%s"', $name, $value) and echo 'data-', $name, '=', '"', $value, '"' + // The quote characters are matched with `.` rather than written into the class, which + // keeps this pattern readable inside a single-quoted PHP string. + $patterns = [ + '/printf\(\s*.data-%s="%s".\s*,(?P[^;]*?)\);/s', + '/echo\s+.data-.\s*,(?P[^;]*?);/s', + ]; + + foreach ($patterns as $pattern) { + preg_match_all($pattern, $source, $matches, PREG_OFFSET_CAPTURE); + + foreach ($matches['args'] as [$args, $offset]) { + if (str_contains($args, '$_e(')) { + continue; + } + + $unescaped[] = sprintf( + '%s:%d %s', + basename($template), + substr_count(substr($source, 0, $offset), "\n") + 1, + trim(preg_replace('/\s+/', ' ', $args)) + ); + } + } + + self::assertSame( + [], + $unescaped, + "a data attribute built from a caller-supplied map went out unescaped:\n" . implode("\n", $unescaped) + ); + } + /** * Inside a `