fix: gotenberg_font_face quotes escaped in HTML context#267
Conversation
9319d21 to
f522a8e
Compare
gotenberg_font_face was declared is_safe => ['css'] only, causing Twig's HTML auto-escaping to convert double quotes to " when the function was used inside a <style> tag. Each method now manages its own htmlspecialchars sanitization independently, removing the shared generateFontFace helper that obscured the security boundary tied to is_safe declarations.
f522a8e to
a37d862
Compare
| $name = htmlspecialchars($name); | ||
| $basename = htmlspecialchars(basename($path)); | ||
|
|
||
| return '<style>@font-face {font-family: "'.$name.'";src: url("'.$basename.'");}</style>'; |
There was a problem hiding this comment.
why duplicate the escaping logic?
I'm not sure I see the benefit here?
There was a problem hiding this comment.
good point! sorry for this, it's useless
| use Twig\Loader\ArrayLoader; | ||
| use Twig\RuntimeLoader\FactoryRuntimeLoader; | ||
|
|
||
| class GotenbergExtensionTest extends TestCase |
There was a problem hiding this comment.
Thanks for the tests!
Would you be able to also add tests that verify that the escaping is happening correctly please?
This will prevent potential future changes from sneaking exploits in.
There was a problem hiding this comment.
something along the lines of
$template = $twig->createTemplate('<style>{{ gotenberg_font_face("</style><script>/*foo.ttf*/ alert('hey')</script><style>", "my_font") }}</style>');
for both font face and style tag
There was a problem hiding this comment.
and its best to also do it for the name argument, otherwise we are only testing one
| use Twig\Loader\ArrayLoader; | ||
| use Twig\RuntimeLoader\FactoryRuntimeLoader; | ||
|
|
||
| class GotenbergExtensionTest extends TestCase |
There was a problem hiding this comment.
Existing test about font live in https://github.com/sensiolabs/GotenbergBundle/blob/1.x/tests/Twig/GotenbergRuntimeTest.php
There was a problem hiding this comment.
tests written in the right class, thanks!
|
@marilenaRM : in the original issue linked in your description @Jean-Beru asked if <style>
{{ gotenberg_font_face('vendor/@fontsource-variable/montserrat/files/montserrat-latin-wght-normal.woff2', 'Montserrat Variable') | e('css') }}
</style>would work. It could make things easier to maintain. WDYT ? |
| return basename($path); | ||
| } | ||
|
|
||
| public function getFontStyleTag(string $path, string $name): string |
There was a problem hiding this comment.
Sorry but I don't get it. You update getFontStyleTag but the method related to gotenberg_font_face is getFontFace
There was a problem hiding this comment.
It was confusing since I duplicated the html escaping, but I rolled back the changes in this file
|
| // | e('css') encodes every non-alphanumeric character (e.g. @ → \40, { → \7B) | ||
| // which destroys the structure of the CSS rule | ||
| $this->assertStringNotContainsString('@font-face', $output); | ||
| $this->assertStringContainsString('\40', $output); |
There was a problem hiding this comment.
We should make an assertion on the whole string ($this->assertSame);
| { | ||
| [$twig] = $this->createTwigEnvironment(); | ||
|
|
||
| $output = $twig->createTemplate('<style>{{ gotenberg_font_face("foo.ttf", "my_font") | e("css") }}</style>')->render([]); |
There was a problem hiding this comment.
What about using a DataProvider to provide the expected value and the template?
| private function createRuntime(): array | ||
| { | ||
| $builder = $this->createMock(BuilderAssetInterface::class); | ||
| $runtime = new GotenbergRuntime(); | ||
| $runtime->setBuilder($builder); | ||
|
|
||
| return [$runtime, $builder]; | ||
| } | ||
|
|
||
| /** | ||
| * @return array{Environment, MockObject&BuilderAssetInterface} | ||
| */ | ||
| private function createTwigEnvironment(): array | ||
| { | ||
| [$runtime, $builder] = $this->createRuntime(); | ||
| $builder->method('addAsset'); | ||
|
|
||
| $twig = new Environment(new ArrayLoader(), ['autoescape' => 'html']); | ||
| $twig->addExtension(new GotenbergExtension()); | ||
| $twig->addRuntimeLoader(new FactoryRuntimeLoader([ | ||
| GotenbergRuntime::class => static fn () => $runtime, | ||
| ])); | ||
|
|
||
| return [$twig, $builder]; | ||
| } |
There was a problem hiding this comment.
IMO, these methods add confusion by returning multiple values. We could create the Environment in the newly added test. Combined with a DataProvider, no code will be duplicated.
Jean-Beru
left a comment
There was a problem hiding this comment.
Some minor comments about test description.
| // basename() strips directory parts on /, so </style> in a path naturally loses its </ — | ||
| // but opening tags like <script> survive and must be escaped by htmlspecialchars |
There was a problem hiding this comment.
IMO, we can remove this comment. Tests should not describe method implementation but only make assertions on the result.
| */ | ||
| public static function provideFontRenderingCases(): iterable | ||
| { | ||
| yield 'font_face_in_html_context' => [ |
There was a problem hiding this comment.
The keys allow you to describe the test in a concrete sentence to improve readability. For example: "with gotenberg_font_face in a style tag". Can you update them?
| // names are not processed by basename — the full injection string must be escaped | ||
| yield 'font_face_escapes_malicious_name' => [ |
There was a problem hiding this comment.
This comment can be resumed in the key.
| // | e('css') encodes every non-alphanumeric character (@ → \40, { → \7B, etc.), | ||
| // destroying the structure of the CSS rule — it is the wrong tool for a complete CSS rule | ||
| yield 'font_face_css_escape_filter_breaks_rule' => [ |
There was a problem hiding this comment.
This comment can be resumed in the key.
Description
gotenberg_font_face was declared is_safe => ['css'] only, causing Twig's HTML auto-escaping to convert double quotes to
"when the function was used inside a<style>tag.Each method now manages its own htmlspecialchars sanitization independently, removing the shared generateFontFace helper that obscured the security boundary tied to is_safe declarations.