Skip to content

Commit f6d85e3

Browse files
committed
Fix sanitization issues when rendering Blade, add regression tests
1 parent 567a7b0 commit f6d85e3

File tree

6 files changed

+104
-10
lines changed

6 files changed

+104
-10
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ url(string $url)
5252
title(string $title)
5353
description(string $description)
5454
image(string $url)
55+
type(string $type)
5556
locale(string $locale)
5657

5758
twitterCreator(string $username)
@@ -266,7 +267,7 @@ The `previewify()` method also returns a signed URL to the image, which lets you
266267

267268
> **Note**
268269
> The `previewify:` prefix will be automatically prepended to all provided data keys.
269-
270+
270271
## Examples
271272

272273
### Service Provider
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<meta name="twitter:card" content="summary_large_image">
2-
@if(seo('twitter.creator')) <meta name="twitter:creator" content="@seo('twitter.creator')"> @endif
3-
@if(seo('twitter.site')) <meta name="twitter:site" content="@seo('twitter.site')"> @endif
4-
@if(seo('twitter.title')) <meta name="twitter:title" content="@seo('twitter.title')"> @endif
5-
@if(seo('twitter.description')) <meta name="twitter:description" content="@seo('twitter.description')"> @endif
2+
@if(seo('twitter.creator')) <meta name="twitter:creator" content="@seo('twitter.creator')" /> @endif
3+
@if(seo('twitter.site')) <meta name="twitter:site" content="@seo('twitter.site')" /> @endif
4+
@if(seo('twitter.title')) <meta name="twitter:title" content="@seo('twitter.title')" /> @endif
5+
@if(seo('twitter.description')) <meta name="twitter:description" content="@seo('twitter.description')" /> @endif
66
@if(seo('twitter.image')) <meta name="twitter:image" content="@seo('twitter.image')" /> @endif

resources/views/components/meta.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<meta property="og:type" content="website" />
2323
@endif
2424

25-
@if(seo('site')) <meta property="og:site_name" content="@seo('site')"> @endif
25+
@if(seo('site')) <meta property="og:site_name" content="@seo('site')" /> @endif
2626

2727
@if(seo('locale')) <meta property="og:locale" content="@seo('locale')" /> @endif
2828

src/SEOManager.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ public function rawTag(string $key, string $tag = null): static
262262
/** Add a meta tag. */
263263
public function tag(string $property, string $content): static
264264
{
265+
$content = e($content);
266+
265267
$this->rawTag("meta.{$property}", "<meta property=\"{$property}\" content=\"{$content}\" />");
266268

267269
return $this;
@@ -326,19 +328,26 @@ public function __call(string $name, array $arguments): string|array|null|static
326328
return $this->get($key);
327329
}
328330

329-
/** Render blade directive. */
331+
/**
332+
* Render blade directive.
333+
*
334+
* This is the only method whose output (returned values) is wrapped in e()
335+
* as these values are used in the meta.blade.php file via @seo calls.
336+
*/
330337
public function render(...$args): array|string|null
331338
{
332339
// Flipp and Previewify support more arguments
333340
if (in_array($args[0], ['flipp', 'previewify'], true)) {
334341
$method = array_shift($args);
335342

343+
// The `flipp` and `previewify` methods return image URLs
344+
// so we don't sanitize the returned value with e() here
336345
return $this->{$method}(...$args);
337346
}
338347

339348
// Two arguments indicate that we're setting a value, e.g. `@seo('title', 'foo')
340349
if (count($args) === 2) {
341-
return $this->set($args[0], $args[1]);
350+
return e($this->set($args[0], $args[1]));
342351
}
343352

344353
// An array means we don't return anything, e.g. `@seo(['title' => 'foo'])
@@ -355,7 +364,7 @@ public function render(...$args): array|string|null
355364
}
356365

357366
// A single value means we fetch a value, e.g. `@seo('title')
358-
return $this->get($args[0]);
367+
return e($this->get($args[0]));
359368
}
360369

361370
/** Handle magic get. */

tests/Pest/ExtensionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
expect(seo('twitter.description'))->toBe('bar');
5757
expect(seo('description'))->toBe('baz');
5858

59-
expect(meta())->toContain('<meta name="twitter:title" content="foo">');
59+
expect(meta())->toContain('<meta name="twitter:title" content="foo" />');
6060
});
6161

6262
test('extensions are automatically enabled when values for them are set', function () {

tests/Pest/SanitizationTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
test('opengraph methods properly sanitize input', function (string $method, string $property) {
4+
$unsanitizedContent = 'Testing string " with several \' XSS characters </title> " . \' .';
5+
6+
seo()->{$method}($unsanitizedContent);
7+
8+
$meta = meta();
9+
10+
$sanitizedContent = e($unsanitizedContent);
11+
12+
// These assertions are equivalent, but included for clarity
13+
expect($meta)->not()->toContain('content="Testing string " with several \' XSS characters </title> " . \' ."');
14+
expect($meta)->not()->toContain("content=\"{$unsanitizedContent}\"");
15+
16+
expect($meta)->toContain("<meta property=\"$property\" content=\"{$sanitizedContent}\" />");
17+
expect($meta)->toContain("<meta property=\"$property\" content=\"Testing string &quot; with several &#039; XSS characters &lt;/title&gt; &quot; . &#039; .\" />");
18+
})->with([
19+
['site', 'og:site_name'],
20+
['url', 'og:url'],
21+
['image', 'og:image'],
22+
['type', 'og:type'],
23+
['locale', 'og:locale'],
24+
]);
25+
26+
// The Twitter integration is tested separately as it uses `meta name=""` instead of `meta property=""`
27+
test('the twitter extension properly sanitizes input', function (string $method, $property) {
28+
$unsanitizedContent = 'Testing string " with several \' XSS characters </title> " . \' .';
29+
30+
seo()->{$method}($unsanitizedContent);
31+
32+
$meta = meta();
33+
34+
$sanitizedContent = e($unsanitizedContent);
35+
36+
// These assertions are equivalent, but included for clarity
37+
expect($meta)->not()->toContain('content="Testing string " with several \' XSS characters </title> " . \' ."');
38+
expect($meta)->not()->toContain("content=\"{$unsanitizedContent}\"");
39+
40+
expect($meta)->toContain("<meta name=\"$property\" content=\"{$sanitizedContent}\" />");
41+
expect($meta)->toContain("<meta name=\"$property\" content=\"Testing string &quot; with several &#039; XSS characters &lt;/title&gt; &quot; . &#039; .\" />");
42+
})->with([
43+
['twitterCreator', 'twitter:creator'],
44+
['twitterSite', 'twitter:site'],
45+
['twitterTitle', 'twitter:title'],
46+
['twitterDescription', 'twitter:description'],
47+
['twitterImage', 'twitter:image'],
48+
]);
49+
50+
// This method is tested separately as it adds an extra (<title>) tag
51+
test('the title method properly sanitizes both tags', function () {
52+
$unsanitizedContent = 'Testing string " with several \' XSS characters </title> " . \' .';
53+
54+
seo()->title($unsanitizedContent);
55+
56+
$meta = meta();
57+
58+
$sanitizedContent = e($unsanitizedContent);
59+
60+
// These assertions are equivalent, but included for clarity
61+
expect($meta)->not()->toContain('meta property="og:title" content="Testing string " with several \' XSS characters </title> " . \' ."');
62+
expect($meta)->not()->toContain('<title>Testing string " with several \' XSS characters </title> " . \' ."</title>');
63+
expect($meta)->not()->toContain("meta property=\"og:title\" content=\"{$unsanitizedContent}\"");
64+
expect($meta)->not()->toContain("<title>{$unsanitizedContent}</title>");
65+
66+
expect($meta)->toContain("<title>{$sanitizedContent}</title>");
67+
expect($meta)->toContain("<title>Testing string &quot; with several &#039; XSS characters &lt;/title&gt; &quot; . &#039; .</title>");
68+
expect($meta)->toContain("<meta property=\"og:title\" content=\"{$sanitizedContent}\" />");
69+
expect($meta)->toContain("<meta property=\"og:title\" content=\"Testing string &quot; with several &#039; XSS characters &lt;/title&gt; &quot; . &#039; .\" />");
70+
});
71+
72+
test('seo blade directive calls are sanitized', function () {
73+
seo(['image' => $string = 'Testing string " with several \' XSS characters </title> " . \' .']);
74+
75+
$escaped = e($string);
76+
77+
// Using @seo() to get a value
78+
expect(blade('<img src="@seo(\'image\')">'))
79+
->toBe("<img src=\"{$escaped}\">")
80+
->not()->toBe('<img src="Testing string " with several \' XSS characters </title> " . \' ."');
81+
82+
// Using @seo() to set a value
83+
expect(blade("@seo('description', 'abc \' def &')"))->toBe('abc &#039; def &amp;');
84+
});

0 commit comments

Comments
 (0)