Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/Http/Controllers/CMS/ArticleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public function create()
public function store(CreateArticleRequest $request)
{
$input = $request->all();
if (isset($input['content'])) {
$input['content'] = \Mews\Purifier\Facades\Purifier::clean($input['content']);
}
if ($request->file('foto')) {
$input['thumbnail'] = $this->uploadFile($request, 'foto');
}
Expand Down Expand Up @@ -109,6 +112,9 @@ public function update($id, UpdateArticleRequest $request)
return redirect(route('articles.index'));
}
$input = $request->all();
if (isset($input['content'])) {
$input['content'] = \Mews\Purifier\Facades\Purifier::clean($input['content']);
}
$removeThumbnail = $request->get('remove_thumbnail');
if ($request->file('foto')) {
$input['thumbnail'] = $this->uploadFile($request, 'foto');
Expand Down
6 changes: 6 additions & 0 deletions app/Http/Controllers/CMS/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public function create()
public function store(CreatePageRequest $request)
{
$input = $request->all();
if (isset($input['content'])) {
$input['content'] = \Mews\Purifier\Facades\Purifier::clean($input['content']);
}
if ($request->file('foto')) {
$this->pathFolder .= '/profile';
$input['thumbnail'] = $this->uploadFile($request, 'foto');
Expand Down Expand Up @@ -111,6 +114,9 @@ public function update($id, UpdatePageRequest $request)
return redirect(route('pages.index'));
}
$input = $request->all();
if (isset($input['content'])) {
$input['content'] = \Mews\Purifier\Facades\Purifier::clean($input['content']);
}
$removeThumbnail = $request->get('remove_thumbnail');
if ($request->file('foto')) {
$input['thumbnail'] = $this->uploadFile($request, 'foto');
Expand Down
11 changes: 6 additions & 5 deletions app/Policies/CustomCSPPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public function configure()
])->addDirective(Directive::CONNECT, [
config('app.serverPantau'),
config('app.databaseGabunganUrl'),
])->addDirective(Directive::OBJECT, [
Keyword::NONE,
])->addDirective(Directive::BASE, [
Keyword::SELF,
]);
}

Expand All @@ -65,11 +69,8 @@ public function shouldBeApplied(Request $request, Response $response): bool
config(['csp.enabled' => false]);
}

// jika mode debug aktif maka disable CSP
if (env('APP_DEBUG')) {
config(['csp.enabled' => false]);
}

// CSP tetap aktif di semua environment termasuk debug untuk menjaga keamanan

return config('csp.enabled');
}
}
3 changes: 2 additions & 1 deletion catatan_rilis.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ Di rilis ini, versi 2603.0.0 berisi penambahan dan perbaikan yang diminta penggu
#### Perubahan Teknis

1. [#943](https://github.com/OpenSID/OpenKab/issues/943) N+1 Query problem pada manajemen user.
2. [#969](https://github.com/OpenSID/OpenKab/issues/969) Terapkan CAPTCHA pada Login & Endpoint Auth untuk Batasi Bot/Bruteforce.
2. [#969](https://github.com/OpenSID/OpenKab/issues/969) Terapkan CAPTCHA pada Login & Endpoint Auth untuk Batasi Bot/Bruteforce.
3. [#962](https://github.com/OpenSID/OpenKab/issues/962) Pencegahan Kerentanan XSS (Cross-Site Scripting).
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",
"league/flysystem-ftp": "^3.10",
"mews/captcha": "^3.3",
"mews/purifier": "^3.4",
"openspout/openspout": "^4.24",
"proengsoft/laravel-jsvalidation": "^4.8",
"shetabit/visitor": "^4.1",
Expand Down
63 changes: 34 additions & 29 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@if($errors->any())
<ul class="alert alert-danger">
@foreach($errors->all() as $error)
<li>{!! $error !!}</li>
<li>{!! clean($error) !!}</li>
@endforeach
</ul>
@endif
Expand Down
2 changes: 1 addition & 1 deletion resources/views/web/article.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<!-- Konten Halaman -->
<div class="container-fluid">
<div class="col-lg-12 wow fadeIn" data-wow-delay="0.5s">
{!! $object->content !!}
{!! clean($object->content) !!}
</div>
</div>
<!-- Konten Halaman -->
Expand Down
2 changes: 1 addition & 1 deletion resources/views/web/articles.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class="card-img-top object-fit-cover" alt="{{ $article->title }}">
</svg>
@endif
<div class="card-body">
<p class="card-text">{!! Str::limit($article->content, 100) !!}</p>
<p class="card-text">{{ Str::limit(strip_tags($article->content), 100) }}</p>
<div class="d-flex justify-content-between align-items-center">
<div>
<a href="{{ route('article', $article->slug) }}"
Expand Down
2 changes: 1 addition & 1 deletion resources/views/web/artikel/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<span class="badge bg-primary">{{ $article->kategori_nama ?? 'Kategori' }}</span>
</div>
<div class="card-text flex-grow-1">
{!! Str::words(strip_tags($article->isi ?? ''), 20, '...') !!}
{{ Str::words(strip_tags($article->isi ?? ''), 20, '...') }}
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/web/artikel/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class="fa fa-calendar-alt text-primary me-2"></i>{{ isset($object->tgl_upload) ?

<div class="card-body p-4 p-md-5">
<div class="artikel-content">
{!! $object->isi ?? '' !!}
{!! clean($object->isi ?? '') !!}
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion resources/views/web/page.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<div class="row g-5 align-items-center">

<div class="col-lg-12 wow fadeIn" data-wow-delay="0.5s">
{!! $object->content !!}
{!! clean($object->content) !!}
</div>
</div>
</div>
Expand Down
101 changes: 101 additions & 0 deletions tests/Feature/XssPreventionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace Tests\Feature;

use App\Models\CMS\Article;
use App\Models\CMS\Category;
use App\Models\CMS\Page;
use App\Models\Enums\StatusEnum;
use App\Services\ArtikelService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
use Mockery;
use Tests\BaseTestCase;
use Mews\Purifier\Facades\Purifier;

class XssPreventionTest extends BaseTestCase
{
use DatabaseTransactions;

protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}

public function test_article_content_is_sanitized_on_store()
{
Storage::fake('public');
$category = Category::factory()->create();

$xssPayload = '<p>Normal text</p><script>alert("xss")</script><a href="javascript:alert(1)">Click</a>';

$response = $this->post(route('articles.store'), [
'title' => 'Judul Artikel XSS',
'slug' => 'judul-artikel-xss',
'content' => $xssPayload,
'category_id' => $category->id,
'published_at' => now()->format('d/m/Y'),
'state' => 1,
]);

$response->assertRedirect(route('articles.index'));

$article = Article::where('slug', 'judul-artikel-xss')->first();

$this->assertNotNull($article);
$this->assertStringNotContainsString('<script>', $article->content);
$this->assertStringNotContainsString('javascript:', $article->content);
$this->assertStringContainsString('<p>Normal text</p>', $article->content);
}

public function test_page_content_is_sanitized_on_store()
{
Storage::fake('public');

$xssPayload = '<p>Normal page</p><img src="x" onerror="alert(1)">';

$response = $this->post(route('pages.store'), [
'title' => 'Halaman XSS',
'slug' => 'halaman-xss',
'content' => $xssPayload,
'published_at' => now()->format('d/m/Y'),
'state' => StatusEnum::aktif,
]);

$response->assertRedirect(route('pages.index'));

$page = Page::where('slug', 'halaman-xss')->first();

$this->assertNotNull($page);
$this->assertStringNotContainsString('onerror', $page->content);
$this->assertStringContainsString('<p>Normal page</p>', $page->content);
}

public function test_artikel_opensid_content_is_sanitized_on_view()
{
$this->withoutMiddleware([\App\Http\Middleware\WebsiteEnable::class]);

// Mock the ArtikelService to return XSS payload
$mockService = Mockery::mock(ArtikelService::class);
$mockService->shouldReceive('artikelById')->with(1)->andReturn((object) [
'id' => 1,
'judul' => 'Detail Test Artikel XSS',
'isi' => 'Konten detail <script>alert(1)</script><iframe src="javascript:alert(1)"></iframe>',
'id_kategori' => 1,
'kategori_nama' => 'Berita Desa',
'tgl_upload' => '2023-10-01 10:00:00',
'enabled' => 1,
]);

$this->app->instance(ArtikelService::class, $mockService);

$response = $this->get(route('web.artikel.show', ['id' => 1]));
$response->assertStatus(200);
$response->assertViewIs('web.artikel.show');

// Assert view does not contain script tags in the output
$response->assertDontSee('<script>alert(1)</script>', false);
$response->assertDontSee('javascript:alert(1)', false);
}
}