Skip to content

Commit 534937f

Browse files
Patches XSS & CSRF (#1078)
* Patched XSS & CSRF * Improve markdown rendering * wip --------- Co-authored-by: Dries Vints <[email protected]>
1 parent 5929a60 commit 534937f

File tree

16 files changed

+64
-93
lines changed

16 files changed

+64
-93
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ APP_NAME="Laravel.io"
22
APP_ENV=local
33
APP_KEY=
44
APP_DEBUG=true
5-
APP_URL=http://laravel.io.test
5+
APP_HOST=laravel.io.test
6+
APP_URL=http://${APP_HOST}
67

78
DB_DATABASE=laravel
89
DB_USERNAME=root

app/Http/Middleware/VerifyCsrfToken.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace App\Http\Middleware;
44

55
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
6+
use Override;
7+
use Symfony\Component\HttpFoundation\Cookie;
68

79
class VerifyCsrfToken extends Middleware
810
{
@@ -14,4 +16,21 @@ class VerifyCsrfToken extends Middleware
1416
protected $except = [
1517
//
1618
];
19+
20+
#[Override]
21+
protected function newCookie($request, $config)
22+
{
23+
return new Cookie(
24+
'XSRF-TOKEN',
25+
$request->session()->token(),
26+
$this->availableAt(60 * $config['lifetime']),
27+
$config['path'],
28+
$config['domain'],
29+
$config['secure'],
30+
true,
31+
false,
32+
$config['same_site'] ?? null,
33+
$config['partitioned'] ?? false
34+
);
35+
}
1736
}

app/Livewire/Editor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public function getUsers($query): array
7373

7474
public function getPreviewProperty(): string
7575
{
76-
return replace_links(md_to_html($this->body ?: ''));
76+
return md_to_html($this->body ?: '');
7777
}
7878

7979
public function preview(): void

app/Markdown/LeagueConverter.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66

77
final class LeagueConverter implements Converter
88
{
9-
public function __construct(
10-
private MarkdownConverter $converter
11-
) {
9+
public function __construct(private MarkdownConverter $converter)
10+
{
1211
}
1312

1413
public function toHtml(string $markdown): string

app/Markdown/MarkdownServiceProvider.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Illuminate\Support\ServiceProvider;
66
use League\CommonMark\Environment\Environment;
77
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
8+
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
9+
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
810
use League\CommonMark\Extension\Mention\MentionExtension;
911
use League\CommonMark\MarkdownConverter;
1012

@@ -15,17 +17,26 @@ public function register(): void
1517
$this->app->singleton(Converter::class, function () {
1618
$environment = new Environment([
1719
'html_input' => 'escape',
20+
'max_nesting_level' => 10,
21+
'allow_unsafe_links' => false,
1822
'mentions' => [
1923
'username' => [
2024
'prefix' => '@',
2125
'pattern' => '[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)',
2226
'generator' => config('app.url').'/user/%s',
2327
],
2428
],
29+
'external_link' => [
30+
'internal_hosts' => config('app.host'),
31+
'open_in_new_window' => true,
32+
'nofollow' => 'external',
33+
],
2534
]);
2635

2736
$environment->addExtension(new CommonMarkCoreExtension);
37+
$environment->addExtension(new GithubFlavoredMarkdownExtension);
2838
$environment->addExtension(new MentionExtension);
39+
$environment->addExtension(new ExternalLinkExtension);
2940

3041
return new LeagueConverter(new MarkdownConverter($environment));
3142
});

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
"spatie/laravel-schedule-monitor": "^3.1",
3737
"spatie/laravel-sitemap": "^7.1",
3838
"symfony/http-client": "^7.0",
39-
"symfony/mailgun-mailer": "^7.0",
40-
"yarri/link-finder": "^2.5"
39+
"symfony/mailgun-mailer": "^7.0"
4140
},
4241
"require-dev": {
4342
"fakerphp/faker": "^1.10",

composer.lock

Lines changed: 1 addition & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/app.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
|
5656
*/
5757

58+
'host' => env('APP_HOST', 'localhost'),
59+
5860
'url' => env('APP_URL', 'http://localhost'),
5961

6062
'asset_url' => env('ASSET_URL', null),

phpunit.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<php>
1919
<env name="APP_ENV" value="testing"/>
2020
<env name="APP_KEY" value="base64:NXoQgjw2ZlOxnGbo5ZRhYgTdM6xLYsgYElNAgcTQJkE="/>
21+
<env name="APP_HOST" value="localhost"/>
2122
<env name="APP_URL" value="http://localhost"/>
2223
<env name="CACHE_DRIVER" value="array"/>
2324
<env name="DB_CONNECTION" value="sqlite"/>

resources/helpers.php

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function is_active(mixed $routes): bool
2222

2323
if (! function_exists('md_to_html')) {
2424
/**
25-
* Convert Markdown to HTML.
25+
* Converts Markdown to a safe HTML string.
2626
*/
2727
function md_to_html(string $markdown): string
2828
{
@@ -42,18 +42,6 @@ function route_to_reply_able(mixed $replyAble): string
4242
}
4343
}
4444

45-
if (! function_exists('replace_links')) {
46-
/**
47-
* Convert Standalone Urls to HTML.
48-
*/
49-
function replace_links(string $markdown): string
50-
{
51-
return (new LinkFinder([
52-
'attrs' => ['target' => '_blank', 'rel' => 'nofollow'],
53-
]))->processHtml($markdown);
54-
}
55-
}
56-
5745
if (! function_exists('canonical')) {
5846
/**
5947
* Generate a canonical URL to the given route and allowed list of query params.

resources/macros/blade.php

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,10 @@
22

33
use Illuminate\Support\Facades\Blade;
44

5-
Blade::directive('md', function ($expression) {
6-
return "<?php echo md_to_html($expression); ?>";
7-
});
8-
95
Blade::directive('error', function ($expression) {
106
return "<?php echo \$errors->first($expression, '<span class=\"block text-sm text-red-500 mt-2\">:message</span>'); ?>";
117
});
128

13-
Blade::directive('formGroup', function ($expression) {
14-
return "<div class=\"form-group<?php echo \$errors->has($expression) ? ' has-error' : '' ?>\">";
15-
});
16-
17-
Blade::directive('endFormGroup', function ($expression) {
18-
return '</div>';
19-
});
20-
219
Blade::directive('title', function ($expression) {
2210
return "<?php \$title = $expression ?>";
2311
});

resources/views/articles/show.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class="w-full bg-center {{ $article->hasHeroImage() ? 'bg-cover' : '' }} bg-gray
106106
x-init="$nextTick(function () { highlightCode($el); })"
107107
class="prose prose-lg text-gray-800 prose-lio"
108108
>
109-
<x-buk-markdown>{!! $article->body() !!}</x-buk-markdown>
109+
<x-buk-markdown>{!! md_to_html($article->body()) !!}</x-buk-markdown>
110110
</div>
111111

112112
@if ($article->isUpdated())

resources/views/components/threads/thread.blade.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@
5555
class="prose prose-lio max-w-none p-6 break-words"
5656
x-data="{}"
5757
x-init="$nextTick(function () { highlightCode($el); })"
58-
x-html="{{ json_encode(replace_links(md_to_html($thread->body()))) }}"
59-
>
60-
</div>
58+
x-html="{{ json_encode(md_to_html($thread->body())) }}"
59+
></div>
6160

6261
@if ($thread->isUpdated())
6362
<div class="text-sm text-gray-900 p-6">

resources/views/livewire/edit-reply.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
x-init="$nextTick(function () { highlightCode($el); }); $watch('edit', function () { highlightCode($el); }); $wire.on('replyEdited', function () { show = true; edit = false; });"
66
class="prose prose-lio max-w-none p-6 break-words"
77
>
8-
{!! replace_links(md_to_html($reply->body())) !!}
8+
{!! md_to_html($reply->body()) !!}
99
</div>
1010

1111
@can(App\Policies\ReplyPolicy::UPDATE, $reply)

tests/Feature/ForumTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@
233233

234234
$this->get("/forum/{$thread->slug}")
235235
->assertSee(new HtmlString(
236-
'<a href="https://github.com/laravelio/laravel.io" rel="nofollow" target="_blank">https://github.com/laravelio/laravel.io</a>'
236+
'<a rel="nofollow noopener noreferrer" target="_blank" href="https://github.com/laravelio/laravel.io">https://github.com/laravelio/laravel.io</a>'
237237
));
238238
});
239239

@@ -245,7 +245,7 @@
245245
Reply::factory()->create(['replyable_id' => $thread->id()]);
246246

247247
$this->get("/forum/{$thread->slug()}")
248-
->assertSee(new HtmlString('&quot;&lt;p&gt;&lt;a href=\&quot;https:\/\/github.com\/laravelio\/laravel.io\&quot; rel=\&quot;nofollow\&quot; target=\&quot;_blank\&quot;&gt;https:\/\/github.com\/laravelio\/laravel.io&lt;\/a&gt;'));
248+
->assertSee(new HtmlString('&quot;&lt;p&gt;&lt;a rel=\&quot;nofollow noopener noreferrer\&quot; target=\&quot;_blank\&quot; href=\&quot;https:\/\/github.com\/laravelio\/laravel.io\&quot;&gt;https:\/\/github.com\/laravelio\/laravel.io&lt;\/a&gt;'));
249249
});
250250

251251
test('an invalid filter defaults to the most recent threads', function () {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
use Tests\TestCase;
4+
5+
uses(TestCase::class);
6+
7+
test('converts markdown to safe html', function () {
8+
$body = 'Hello, World! ![](image.png).';
9+
10+
expect(md_to_html($body))->toBe('<p>Hello, World! <img src="image.png" alt="" />.</p>' . "\n");
11+
});
12+
13+
test('prevents unsafe links', function () {
14+
$body = "[Unsafe Link](javascript:alert('Hello'))";
15+
16+
expect(md_to_html($body))->toBe('<p><a>Unsafe Link</a></p>' . "\n");
17+
});

0 commit comments

Comments
 (0)