• Czas czytania ~12 min
  • 20.01.2023

Zasady zabezpieczeń zawartości (CSP) to świetny sposób na zwiększenie bezpieczeństwa aplikacji Laravel. Umożliwiają one umieszczenie na białej liście źródeł skryptów, stylów i innych zasobów, które mogą być ładowane przez strony internetowe. Zapobiega to wstrzyknięciu złośliwego kodu przez osobę atakującą do widoku użytkownika (a w konsekwencji do przeglądarek użytkowników) i może dać dodatkową pewność, że zasoby innych firm, których używasz, są tym, czego zamierzasz użyć.

W tym artykule przyjrzymy się, czym jest CSP i co osiąga. Następnie przyjrzymy się, jak użyć pakietu spatie/laravel-csp, aby dodać CSP do aplikacji Laravel. Omówimy również pokrótce kilka wskazówek ułatwiających dodawanie dostawcy CSP do istniejącej aplikacji.

Co to jest polityka bezpieczeństwa treści?

Mówiąc najprościej, dostawca CSP to tylko zestaw reguł, które są zwykle zwracane z serwera do przeglądarki klienta za pośrednictwem nagłówka Content-Security-Policy w odpowiedzi. Pozwala nam, jako programistom, określić, jakie zasoby przeglądarka może załadować.

W rezultacie może to dać nam pewność, że nasi użytkownicy ładują do przeglądarki tylko te obrazy, czcionki, style i skrypty, które uznaliśmy za bezpieczne i na ich użycie. Jeśli przeglądarka spróbuje wczytać zasób, który jest niedozwolony, zostanie on zablokowany.

Korzystanie z dobrze skonfigurowanych zasad zabezpieczeń zawartości może zmniejszyć prawdopodobieństwo kradzieży danych użytkownika i innych złośliwych działań przeprowadzanych przy użyciu ataków, takich jak skrypty międzywitrynowe (XSS).

Dostawcy CSP mogą stać się bardzo złożeni (szczególnie w większych aplikacjach), ale są istotną częścią bezpieczeństwa każdej aplikacji.

Jak zaimplementować CSP w Laravel

Jak już wspomnieliśmy, CSP to tylko zestaw reguł, które są zwracane z serwera do przeglądarki klienta za pośrednictwem nagłówka w odpowiedzi lub czasami definiowane jako tag <meta> w kodzie HTML. Oznacza to, że istnieje kilka sposobów zastosowania dostawcy CSP do aplikacji. Na przykład możesz zdefiniować nagłówki w konfiguracji serwera (np. - Nginx). Może to być jednak kłopotliwe i trudne do zarządzania, więc uważam, że łatwiej jest zarządzać zasadami na poziomie aplikacji.

Zazwyczaj najprostszym sposobem dodania polityki do aplikacji Laravel jest użycie pakietu < href="https://github.com/spatie/laravel-csp">spatie/laravel-csp. Rzućmy więc okiem na to, jak możemy z niego korzystać i różne opcje, które nam zapewnia.

Instalacja

Aby rozpocząć korzystanie z pakietu spatie/laravel-csp, musimy najpierw zainstalować go przez Composer za pomocą następującego polecenia:

composer require spatie/laravel-csp

Następnie możemy opublikować plik konfiguracyjny pakietu za pomocą następującego polecenia:

php artisan vendor:publish --tag=csp-config

Uruchomienie powyższego polecenia powinno utworzyć nowy plik config/csp.php dla Ciebie.

Stosowanie zasad do odpowiedzi

Po zainstalowaniu pakietu musimy upewnić się, że nagłówek Content-Security-Policy został dodany do odpowiedzi HTTP. Istnieje kilka różnych sposobów, w zależności od aplikacji.

Jeśli chcesz zastosować dostawcę CSP do wszystkich tras sieci Web, możesz dodać klasę oprogramowania pośredniczącego Spatie\Csp\AddCspHeaders do składnika web part tablicy $middlewareGroups w aplikacji/Http/Kernel.php plik:

// ...
protected $middlewareGroups = [
   'web' => [
       // ...
       \Spatie\Csp\AddCspHeaders::class,
   ],
// ...

W wyniku tej czynności każda trasa przebiegająca przez grupę oprogramowania pośredniczącego sieci Web będzie miała automatycznie dodany nagłówek CSP.

Jeśli wolisz dodać dostawcę CSP do poszczególnych tras lub grup tras, możesz zamiast tego użyć oprogramowania pośredniczącego w pliku .php sieci Web. Na przykład, jeśli chcemy zastosować oprogramowanie pośredniczące tylko do określonej trasy, możemy zrobić coś takiego

use Spatie\Csp\AddCspHeaders;

Route::get('example-route', 'ExampleController')->middleware(AddCspHeaders::class);

: Lub, jeśli chcemy zastosować oprogramowanie pośredniczące do grupy tras, możemy wykonać następujące czynności:

use Spatie\Csp\AddCspHeaders;

Route::middleware(AddCspHeaders::class)->group(function () {
    // Routes go here...
});

Domyślnie, jeśli nie zdefiniujesz jawnie zasady, która powinna być używana z oprogramowaniem pośredniczącym, zostanie użyta zasada zdefiniowana w domyślnym kluczu opublikowanego pliku config/csp.php. Możesz więc zaktualizować to pole, jeśli chcesz użyć własnych zasad domyślnych.

Możliwe, że masz kilka zasad zabezpieczeń zawartości dla swojej aplikacji lub witryny sieci Web. Na przykład możesz mieć dostawcę CSP do użytku na publicznych stronach witryny i innego dostawcę CSP do użytku w zamkniętych częściach witryny. Może to być spowodowane użyciem różnych zestawów zasobów (takich jak skrypty, style i czcionki) w każdym z tych miejsc. Gdybyśmy więc chcieli jawnie zdefiniować zasady,

które powinny być używane dla określonej trasy, możemy wykonać następujące czynności:

use App\Support\Csp\Policies\CustomPolicy;
use Spatie\Csp\AddCspHeaders;

Route::get('example-route', 'ExampleController')->middleware(AddCspHeaders::class.':'.CustomPolicy::class);

Podobnie możemy również jawnie zdefiniować politykę w grupie tras:

use App\Support\Csp\Policies\CustomPolicy;
use Spatie\Csp\AddCspHeaders;

Route::middleware(AddCspHeaders::class.':'.CustomPolicy::class)->group(function () {
    // Routes go here...
});

Korzystanie z domyślnej zasady bezpieczeństwa zawartości

Pakiet jest dostarczany z domyślną zasadą Spatie\Csp\Policies\Basic, która definiuje już kilka reguł. Polityka pozwala nam ładować tylko obrazy, czcionki, style i skrypty z tej samej domeny, co nasza aplikacja. Jeśli używasz tylko zasobów ładowanych z własnej domeny, ta zasada może Ci wystarczyć.

Podstawowa zasada utworzy nagłówek Content-Security-Policy, który wygląda mniej więcej tak:

ase-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self';media-src 'self';object-src 'none';script-src 'self' 'nonce-YKXiTcrg6o4DuumXQDxYRv9gHPlZng6z';style-src 'self' 'nonce-YKXiTcrg6o4DuumXQDxYRv9gHPlZng6z'

Tworzenie własnych zasad bezpieczeństwa zawartości

W zależności od aplikacji można utworzyć własne zasady, aby umożliwić ładowanie innych zasobów, które są dozwolone przez zasadę podstawową.

Jak już wspomnieliśmy, istnieje wiele reguł, które można zdefiniować w CSP i mogą one szybko stać się stosunkowo złożone. Aby pomóc Ci w krótkim zrozumieniu, przyjrzymy się kilku typowym regułom, których prawdopodobnie będziesz używać we własnej aplikacji.

Na potrzeby tego przewodnika założymy, że mamy projekt, który wykorzystuje następujące zasoby na stronie:

  • Plik JavaScript dostępny w domenie witryny pod adresem: /js/app.js.
  • Plik JavaScript dostępny zewnętrznie pod adresem: https://unpkg.com/vue@3/dist/vue.global.js.
  • Inline JavaScript - Ale nie tylko wbudowany JavaScript, chcemy zezwolić tylko na wbudowaną obsługę JavaScript, na której uruchamianie wyraźnie zezwoliliśmy.
  • Plik CSS dostępny w domenie serwisu pod adresem: /css/app.css.
  • Plik CSS dostępny zewnętrznie pod adresem: https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
  • Obraz dostępny w domenie serwisu pod adresem: /img/hero.png.
  • Obraz dostępny zewnętrznie pod adresem: https://laravel.com/img/logotype.min.svg.

Utworzymy zasady bezpieczeństwa treści, które zezwalają tylko na ładowanie powyższych elementów na naszej stronie. Jeśli przeglądarka spróbuje załadować inny zasób, żądanie zostanie zablokowane i nie zostanie załadowane.

Podstawowy widok bloku dla strony może wyglądać mniej więcej tak:

<html>
    <head>
        <title>CSP Test</title>

        {{-- Load Vue.js from the CDN --}}
        <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

        {{-- Load some JS scripts from our domain --}}
        <script src="{{ asset('js/app.js') }}"></script>

        {{-- Load Bootstrap 5 CSS --}}
        <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
              rel="stylesheet"
              integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
              crossorigin="anonymous"
        >
        {{-- Load a CSS file from our own domain --}}
        <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    </head>

    <body>
        <h1>Csp Header</h1>

        <img src="{{ asset('img/hero.png') }}" alt="CSP hero image">

        <img src="https://laravel.com/img/logotype.min.svg" alt="Laravel logo">

        {{-- Define some JS directly in our HTML. --}}
        <script>
            console.log('Loaded inline script!');
        </script>

        {{-- Evil JS script which we didn't write ourselves and was injected by another script! --}}
        <script>
            console.log('Injected malicious script! ☠️');
        </script>
    </body>
</html>

Aby rozpocząć, najpierw należy utworzyć własną klasę zasad, która rozszerza klasę Spatie\Csp\Policies\Basic pakietu. Nie ma określonego katalogu, w którym musisz go umieścić, więc możesz wybrać miejsce, które najlepiej pasuje do Twojej aplikacji. Lubię umieszczać moje w katalogu app/Support/Csp/Policy, ale to tylko moje preferencje. Więc stworzę nową aplikację / Wsparcie / CSP / Policies / CustomPolicy.php plik:

namespace App\Support\Csp\Policies;

use Spatie\Csp\Policies\Basic;

class CustomPolicy extends Basic
{
    public function configure()
    {
        parent::configure();

        // We can add our own policy directives here...
    }
}

Jak widać z komentarza w powyższym kodzie, możemy umieścić własne niestandardowe dyrektywy w metodzie configure.

Dodajmy więc kilka dyrektyw i przyjrzyjmy się, co robią:

namespace App\Support\Csp\Policies;

use Spatie\Csp\Policies\Basic;

class CustomPolicy extends Basic
{
    public function configure()
    {
        parent::configure();

        $this->addDirective(Directive::SCRIPT, ['https://unpkg.com/vue@3/'])
            ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net/npm/[email protected]/'])
            ->addDirective(Directive::IMG, 'https://laravel.com');
    }
}

Powyższa polityka utworzy nagłówek Content-Security-Policy, który wygląda mniej więcej tak:

ase-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self';media-src 'self';object-src 'none';script-src 'self' 'nonce-3fvDDho6nNJ3xXPcK3VMsgBWjVTJzijk' https://unpkg.com/vue@3/;style-src 'self' 'nonce-3fvDDho6nNJ3xXPcK3VMsgBWjVTJzijk' https://cdn.jsdelivr.net/npm/[email protected]/

W powyższym przykładzie zdefiniowaliśmy, że można załadować dowolny plik JS ładowany z adresu URL zaczynającego się od https://unpkg.com/vue@3/. Oznacza to, że nasz skrypt Vue.js będzie mógł załadować się zgodnie z oczekiwaniami.

Zezwoliliśmy również na wczytywanie wszystkich plików CSS ładowanych z adresu URL zaczynającego się od https://cdn.jsdelivr.net/npm/[email protected]/.

Ponadto zezwoliliśmy na wczytywanie wszystkich obrazów pobranych z adresu URL rozpoczynającego się od https://laravel.com.

Być może zastanawiasz się również, gdzie są dyrektywy umożliwiające uruchamianie wbudowanego JavaScriptu oraz ładowanie obrazów, CSS i plików JS z naszej domeny. Wszystkie są uwzględnione w podstawowej polityce, więc nie musimy ich dodawać samodzielnie. Dzięki temu możemy zachować naszą politykę celną i oszczędną i dodawać tylko te dyrektywy, których potrzebujemy (zazwyczaj dla aktywów zewnętrznych).

Jednak w tej chwili, gdybyśmy spróbowali uruchomić nasz wbudowany JavaScript, nie zadziała. Omówimy, jak to naprawić poniżej.

Chociaż powyższe reguły działają i pozwolą na załadowanie naszej strony zgodnie z oczekiwaniami, możesz je zaostrzyć, aby jeszcze bardziej zwiększyć bezpieczeństwo strony.

Wyobraźmy sobie, że z jakiegoś nieznanego powodu złośliwy skrypt zdołał dotrzeć do adresu URL zaczynającego się od https://unpkg.com/vue@3/, takiego jak https://unpkg.com/vue@3/malicious-script.js. Ze względu na obecną konfigurację naszych zasad, skrypt ten będzie mógł być uruchamiany na naszej stronie. Zamiast tego możemy jawnie zdefiniować dokładny adres URL skryptu, na który chcemy zezwolić.

Zaktualizujemy nasze zasady, aby uwzględnić dokładne adresy URL skryptów, stylów i obrazów, które chcemy załadować:

namespace App\Support\Csp\Policies;

use Spatie\Csp\Policies\Basic;

class CustomPolicy extends Basic
{
    public function configure()
    {
        parent::configure();

        $this->addDirective(Directive::SCRIPT, ['https://unpkg.com/vue@3/dist/vue.global.js'])
            ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'])
            ->addDirective(Directive::IMG, 'https://laravel.com/img/logotype.min.svg');
    }
}

Powyższa polityka utworzy nagłówek Content-Security-Policy, który wygląda mniej więcej tak:

ase-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.com/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://unpkg.com/vue@3/dist/vue.global.js;style-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css

Korzystając z powyższego podejścia, możemy znacznie poprawić bezpieczeństwo naszej strony, ponieważ teraz zezwalamy tylko na dokładne skrypty, style, i obrazy, które chcemy załadować.

Jednak, jak można sobie wyobrazić, robienie tego w przypadku większych projektów może stać się żmudne i czasochłonne, ponieważ będziesz musiał zdefiniować każdy pojedynczy zasób, który ładujesz ze źródła zewnętrznego. Jest to więc coś, co musisz rozważyć na podstawie projektu po projekcie.

Dodawanie Nonces do dostawcy CSP

Teraz, gdy przyjrzeliśmy się, jak możemy zezwolić na ładowanie zasobów zewnętrznych, musimy również przyjrzeć się, jak możemy zezwolić na uruchamianie skryptów wbudowanych.

Być może pamiętasz, że mieliśmy dwa wbudowane bloki skryptów w naszym widoku Blade powyżej: jeden, który

  • ładował JS, który zamierzaliśmy uruchomić
  • , jeden, który został wstrzyknięty przez złośliwy skrypt i uruchomił zły kod!

Skrypt został dodany na dole widoku Blade wyglądał następująco:

<html>
        <!-- ... -->

        {{-- Define some JS directly in our HTML. --}}
        <script>
            console.log('Loaded inline script!');
        </script>

        {{-- Evil JS script which we didn't write ourselves and was injected by another script! --}}
        <script>
            console.log('Injected malicious script! ☠️');
        </script>
    </body>
</html>

Aby umożliwić uruchamianie skryptów wbudowanych, możemy użyć "nonces". Nonce to losowy ciąg generowany dla każdego żądania. Ten ciąg jest następnie dodawany do nagłówka CSP (dodawanego za pomocą zasad podstawowych, które rozszerzamy), a każdy ładowany skrypt wbudowany musi zawierać ten nonce w swoim atrybucie nonce.

Zaktualizujmy nasz widok bloku, aby uwzględnić nonce dla naszego bezpiecznego skryptu wbudowanego za pomocą pomocnika csp_nonce() dostarczonego przez pakiet:

<html>
        <!-- ... -->

        {{-- Define some JS directly in our HTML. --}}
        <script nonce="{{ csp_nonce() }}">
            console.log('Loaded inline script!');
        </script>

        {{-- Evil JS script which we didn't write ourselves and was injected by another script! --}}
        <script>
            console.log('Injected malicious script! ☠️');
        </script>
    </body>
</html>

W wyniku tego nasz bezpieczny skrypt wbudowany będzie teraz uruchamiany zgodnie z oczekiwaniami. Natomiast wstrzyknięty skrypt, który nie ma atrybutu nonce, zostanie zablokowany przed uruchomieniem.

Korzystanie z metatagu

Jest to mało prawdopodobne, ale możliwe, że zawartość nagłówka Content-Security-Policy przekracza maksymalną dozwoloną długość. W takim przypadku możemy dodać metatag do naszej strony, który wyświetla reguły dla naszej przeglądarki.

Aby to zrobić, możesz dodać dyrektywę @cspMetaTag Blade pakietu do tagu <head> widoku, w następujący sposób:

<html>
    <head>
        <!-- ... -->

        @cspMetaTag(App\Support\Csp\Policies\CustomPolicy::class)
    </head>

    <!-- ... -->

</html>

Korzystając z powyższego przykładu CustomPolicy, spowoduje to wyświetlenie następującego metatagu:

<meta http-equiv="Content-Security-Policy" content="base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.com/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://unpkg.com/vue@3/dist/vue.global.js;style-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">

Porady dotyczące implementowania dostawcy CSP w istniejącej aplikacji Laravel

Dodawanie dostawcy CSP do istniejącej aplikacji może być czasami dość trudnym zadaniem. Bardzo łatwo jest złamać interfejs użytkownika, implementując zbyt rygorystyczny dostawca CSP lub zapominając dodać regułę dla określonego zasobu, który może być używany tylko na jednej stronie. Podniosę rękę i przyznam, że sam już to robiłem.

Więc jeśli masz szansę zaimplementować CSP przy pierwszym uruchomieniu nowej aplikacji, gorąco polecam to zrobić. O wiele łatwiej jest napisać politykę wraz z budowaniem aplikacji. Istnieje mniejsze prawdopodobieństwo, że zapomnisz dodać określone reguły, a nawet możesz dodać reguły zasad w tym samym zatwierdzeniu git, co dodałeś zasób, aby móc łatwo śledzić go w przyszłości.

Jeśli jednak dodajesz dostawcę CSP do istniejącej aplikacji, możesz wykonać kilka czynności, aby ułatwić ten proces sobie i użytkownikom.

Po pierwsze, możesz włączyć tryb "tylko raport" dla swojej polityki. Dzięki temu możesz zdefiniować zasady, ale za każdym razem, gdy którakolwiek z reguł zostanie naruszona (np. wczytanie zasobu, którego wczytanie nie zostało dozwolone), raport zostanie wysłany na dany adres URL, zamiast blokować ładowanie zasobu. W ten sposób można utworzyć dostawcę CSP, którego chcesz użyć, i przetestować go w środowisku produkcyjnym bez przerywania aplikacji dla użytkowników. Następnie możesz użyć raportów, by zidentyfikować wszystkie zasoby, które zostały pominięte, i dodać je do zasad.

Aby włączyć raportowanie dla zasad, musisz najpierw ustawić adres URL, pod którym ma zostać wysłane żądanie po wykryciu naruszenia. Można to dodać, ustawiając pole CSP_REPORT_URI w pliku env w następujący sposób:

CSP_REPORT_URI=https://example.com/report-sent-here

Następnie można użyć metody reportOnly w zasadach. Gdybyśmy zaktualizowali nasze zasady, aby zgłaszać tylko naruszenia, wyglądałoby to tak:

namespace App\Support\Csp\Policies;

use Spatie\Csp\Policies\Basic;

class CustomPolicy extends Basic
{
    public function configure()
    {
        parent::configure();

        $this->addDirective(Directive::SCRIPT, ['https://unpkg.com/vue@3/dist/vue.global.js'])
            ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'])
            ->addDirective(Directive::IMG, 'https://laravel.com/img/logotype.min.svg')
            ->reportOnly();
    }
}

W wyniku użycia metody reportOnly do odpowiedzi zostanie dodany nagłówek Content-Security-Policy-Report-Only zamiast nagłówka Content-Security-Policy. Powyższa polityka wygeneruje nagłówek, który wygląda następująco:

eport-uri https://example.com/report-sent-here;base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.com/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://unpkg.com/vue@3/dist/vue.global.js;style-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css

Po pewnym czasie (może kilka dni, tydzień lub miesiące), jeśli nie otrzymałeś żadnych raportów i masz pewność, że zasady są odpowiednie, możesz je włączyć. Oznacza to, że nadal będziesz mieć możliwość otrzymywania raportów w przypadku naruszenia, ale będziesz także mógł uzyskać pełne korzyści bezpieczeństwa wynikające z wdrożenia zasad, ponieważ wszelkie naruszenia będą blokowane. W tym celu można usunąć wywołanie metody reportOnly z klasy

zasad.Ponadto przydatne może być stopniowe zwiększanie surowości reguł, jak opisano wcześniej w tym artykule. Może więc chcieć używać tylko domen lub symboli wieloznacznych w początkowym dostawcy CSP, a następnie stopniowo zmieniać reguły, aby używać bardziej szczegółowych adresów URL.

Podsumowując, myślę, że kluczem do przyjęcia korzystania z CSP w istniejącej aplikacji jest stopniowe podejście do niego. Zdecydowanie możliwe jest dodanie tego wszystkiego za jednym razem, ale możesz zmniejszyć ryzyko błędów i błędów, przyjmując bardziej stopniowe podejście.

Wniosek

: Mamy nadzieję, że ten artykuł powinien dać Ci przegląd dostawców CSP, problemów, które rozwiązują, i sposobu ich działania. Powinieneś również wiedzieć, jak zaimplementować CSP we własnej aplikacji Laravel przy użyciu pakietu spatie/laravel-csp.

Można również zapoznać się z dokumentacją < href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy">MDN dotyczącą dostawców CSP, która zawiera więcej informacji na temat opcji dostępnych w aplikacjach.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297