abkrim's avatar
Level 13

405 Method Not Allowed in production in one form, any problem on others page forms

Hello

I have a strange behavior in the profile form, in an app with TALL. It works for me locally, and it does not work for me in production But in production it sends me a message:

Oops! An Error Occurred The server returned a "405 Method Not Allowed". Something is broken. Please let us know what you were doing when this error occurred. We will fix it as soon as possible. Sorry for any inconvenience caused.

When debug trace error in js, I can see strange behavior. Pese a que la ruta esta definida como GET, el proceso de alpineJs send a POST method.

fetch(
            `${window.livewire_app_url}/livewire/message/${payload.fingerprint.name}`,
            {
                method: 'POST',
                body: JSON.stringify(payload),
                // This enables "cookies".
                credentials: 'same-origin',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'text/html, application/xhtml+xml',
                    'X-Livewire': true,

                    // We'll set this explicitly to mitigate potential interference from ad-blockers/etc.
                    'Referer': window.location.href,
                    ...(csrfToken && { 'X-CSRF-TOKEN': csrfToken }),
                    ...(socketId && { 'X-Socket-ID': socketId })
                },
            }
        )

The form is a mix of three forms.

  • The one with the profile
  • The one with the password
  • The one with the token

All three subforms are components of livewire.

Each one has their buton, to save.

Thus, the error occurs in any of the three save buttons, in production.

On local work perfectly.

web.php

Route::group(['prefix' => 'dashboard', 'as' => 'admin.', 'middleware' => ['auth']], function () {
   Route::get('/profile', Profile::class)->name('profile');
   ...
});

Profile.php

<?php

namespace App\Http\Livewire\Auth;

use App\Models\User;
use Livewire\Component;

class Profile extends Component
{

    public User $user;

    public function mount() { $this->user = auth()->user(); }

    public function render()
    {
        return view('livewire.auth.profile');
    }
}

profile.blade.php

<div>
    @livewire('auth.profile.update-profile')
    <!-- Contraseña -->
    <div class="hidden sm:block" aria-hidden="true">
        <div class="py-5">
            <div class="border-t border-gray-200"></div>
        </div>
    </div>

    @livewire('auth.profile.update-password')

    <!-- Token -->
    <div class="hidden sm:block" aria-hidden="true">
        <div class="py-5">
            <div class="border-t border-gray-200"></div>
        </div>
    </div>

    @livewire('auth.profile.update-token')
</div>

UpdateProfile.php

<?php

namespace App\Http\Livewire\Auth\Profile;

use App\Models\User;
use Livewire\Component;
use Livewire\WithFileUploads;

class UpdateProfile extends Component
{
    use WithFileUploads;

    public User $user;

    public $upload;

    public function render()
    {
        return view('livewire.auth.profile.update-profile');
    }

    protected function rules(): array
    {
        return [
            'user.name' => [
                'string',
                'required',
                'min:5',
            ],
            'user.email' => [
                'email:rfc',
                'required',
                'regex:/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ix',
                'unique:users,email,' . $this->user->id,
            ],
            'upload' => [
                'nullable',
                'image',
                'mimes:jpg,bmp,png',
                'max:200'
            ],
        ];
    }

    public function mount() { $this->user = auth()->user(); }

    public function save()
    {
        $this->validate();

        $this->user->save();

        $this->upload && $this->user->update([
            'avatar' => $this->upload->store('/', 'avatars'),
        ]);

        $this->emitSelf('notify-saved');
    }

}

update-profile.blade.php

<div>
    <div class="md:grid md:grid-cols-3 md:gap-6 border-gray-300">
        <div class="md:col-span-1">
            <h3 class="text-lg font-medium leading-6 text-gray-900">Perfil</h3>
            <p class="mt-1 text-sm text-gray-500">
                Esta información es privada y sólo tiene efectos administrativos.
            </p>
        </div>
        <div class="mt-5 md:mt-0 md:col-span-2">
            <form wire:submit.prevent="save">
                <div class="shadow sm:rounded-md sm:overflow-hidden">
                    <div class="px-4 py-5 bg-white space-y-6 sm:p-6">
                        <!-- Nombre -->
                        <div class="grid grid-cols-3 gap-6">
                            <div class="col-span-3 sm:col-span-2">
                                <label for="name" class="block text-sm font-medium text-gray-700">
                                    Nombre
                                </label>
                                <div class="mt-1 flex rounded-md shadow-sm">
                                    <input wire:model.defer="user.name" type="text" name="username" id="username" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Nombre y apellidos">
                                </div>
                                <div class="mt-1 relative rounded-md shadow-sm">
                                    @error('user.name')
                                    <div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
                                    @enderror
                                </div>
                            </div>
                        </div>

                        <div class="grid grid-cols-3 gap-6">
                            <div class="col-span-3 sm:col-span-2">
                                <label for="email" class="block text-sm font-medium text-gray-700">
                                    Email
                                </label>
                                <div class="mt-1 flex rounded-md shadow-sm">
                                    <input wire:model.defer="user.email" type="text" name="email" id="email" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Correo electrónico">
                                </div>
                                <div class="mt-1 relative rounded-md shadow-sm">
                                    @error('user.email')
                                    <div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
                                    @enderror
                                </div>
                            </div>
                        </div>

                        <div>
                            <label class="block text-sm font-medium text-gray-700">
                                Foto
                            </label>
                            <div class="mt-1 flex items-center space-x-5">
                                    <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">
                                        @if ($upload)
                                            <img src="{{ $upload->temporaryUrl() }}" alt="Profile Photo">
                                        @else
                                            <img src="{{ auth()->user()->avatarUrl() }}" alt="Profile Photo">
                                        @endif
                                    </span>
                                <input type="file" wire:model="upload" id="photo">
                                <div class="mt-1 relative rounded-md shadow-sm">
                                    @error('user.avatar')
                                    <div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
                                    @enderror
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
                        <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                            Save
                        </button>
                        <span x-data="{ open: false }"
                              x-init="@this.on('notify-saved',
                                    () => {
                                        if (open === false) setTimeout(() => { open = false }, 3500);
                                        open = true;
                                    })"
                              x-show.transition.out.duration.1000ms="open"
                              style="display: none;"
                              class="text-gray-500">¡Guardado!</span>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>

UpdatePassword.php

<?php

namespace App\Http\Livewire\Auth\Profile;

use Livewire\Component;
use App\Models\User;

class UpdatePassword extends Component
{
    //public User $user;

    public $password;
    public $password_confirmation;

    public function render()
    {
        return view('livewire.auth.profile.update-password');
    }

    protected $rules = [
            'password' => [
                'required',
                'confirmed',
                'min:10',
                'regex:/^.*(?=.{3,})(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[\d\x])(?=.*[!$#%]).*$/',
            ],
    ];

    public function updated($propertyName)
    {
        $this->validateOnly($propertyName);
    }

    public function save()
    {
        $this->validate();

        auth()->user()->update([
            'password' => bcrypt($this->password)
        ]);

        $this->emitSelf('notify-saved');

        $this->resetForm();
    }

    protected function resetForm()
    {
        $this->password = '';
        $this->password_confirmation = '';
    }

}

update-password.blade.php

<div>
    <div class="md:grid md:grid-cols-3 md:gap-6 border-gray-300">
        <div class="md:col-span-1">
            <h3 class="text-lg font-medium leading-6 text-gray-900">Contraseña</h3>
            <p class="mt-1 text-sm text-gray-500">
                Puede cambiar su contraseña en este formulario
            </p>
        </div>
        <div class="mt-5 md:mt-0 md:col-span-2">
            <form wire:submit.prevent="save">
                <div class="shadow sm:rounded-md sm:overflow-hidden">
                    <div class="px-4 py-5 bg-white space-y-6 sm:p-6">

                        <div class="grid grid-cols-3 gap-6">
                            <div class="col-span-3 sm:col-span-2">
                                <label for="password" class="block text-sm font-medium text-gray-700">
                                    Contraseña
                                </label>
                                <div class="mt-1 flex rounded-md shadow-sm">
                                    <input wire:model.defer="password" type="password" name="password" id="password" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Nueva contraseña">
                                </div>
                                <div class="mt-1 relative rounded-md shadow-sm">
                                    @error('password')
                                    <div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
                                    @enderror
                                </div>
                            </div>
                        </div>

                        <div class="grid grid-cols-3 gap-6">
                            <div class="col-span-3 sm:col-span-2">
                                <label for="password_confirmation" class="block text-sm font-medium text-gray-700">
                                    Confirma la contraseña
                                </label>
                                <div class="mt-1 flex rounded-md shadow-sm">
                                    <input wire:model.defer="password_confirmation" type="password" name="password_confirmation" id="password_confirmation" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300">
                                </div>
                                <div class="mt-1 relative rounded-md shadow-sm">
                                    @error('password_confirmation')
                                    <div class="mt-1 text-red-500 text-sm">{{ $message }}</div>
                                    @enderror
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
                        <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                            Cambiar
                        </button>
                        <span x-data="{ open: false }"
                              x-init="@this.on('notify-saved',
                                    () => {
                                        if (open === false) setTimeout(() => { open = false }, 3500);
                                        open = true;
                                    })"
                              x-show.transition.out.duration.1000ms="open"
                              style="display: none;"
                              class="text-gray-500">¡Contraseña cambiada!
                        </span>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>

UpdateToken.php

<?php

namespace App\Http\Livewire\Auth\Profile;

use App\Helpers\ApiHelpers;
use Livewire\Component;

class UpdateToken extends Component
{
    public string $token;

    public function mount()
    {
        $this->resetState();
    }

    public function updateToken()
    {

        $user = auth()->user();

        ApiHelpers::deleteTokenUserType($user->id, 'auth_token');

        $this->token = auth()->user()->createToken('auth_token')->plainTextToken;
    }

    public function render()
    {
        return view('livewire.auth.profile.update-token');
    }

    protected function resetState()
    {
        $this->token = '';
    }
}

update-token.blade.php

<div>
    <div class="md:grid md:grid-cols-3 md:gap-6 border-gray-300">
        <div class="md:col-span-1">
            <h3 class="text-lg font-medium leading-6 text-gray-900">Token API</h3>
            <p class="mt-1 text-sm text-gray-500">
                El token solo se muestra una vez generado, por seguridad.
                <br />Copielo y guardelo en un lugar seguro.
                <br />El anterior se elimina del sistema, dejan de ser operativo.
            </p>
        </div>
        <div class="mt-5 md:mt-0 md:col-span-2">
            <form wire:submit.prevent="updateToken">
                <div class="shadow sm:rounded-md sm:overflow-hidden">
                    <div class="px-4 py-5 bg-white space-y-6 sm:p-6">
                        <div class="grid grid-cols-3 gap-6">
                            <div class="col-span-3 sm:col-span-2">
                                <label for="token" class="block text-sm font-medium text-gray-700">
                                    Token
                                </label>
                                <div class="mt-1 flex rounded-md shadow-sm">
                                    <input wire:model="token" type="text" name="token" id="token"  disabled class="disabled:opacity-50 focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="Haz click en el botón para regenerar el token">
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
                        <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                            Regenerar
                        </button>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>
0 likes
12 replies
bugsysha's avatar

Have you checked if the request is made to the correct route?

abkrim's avatar
Level 13

Hi. On post ...

  • Route get in web.php

Double check

a route:list | grep -u profile
|        | GET|HEAD                               | dashboard/profile                     | admin.profile                      | App\Http\Livewire\Auth\Profile                                           | web   

Of course all caches and routes cleared after update.

And principal.

If work in local, why it's not work in production?

Tray2's avatar

@abkrim Unless your development server and your production server has identical setups and permissions on the file system they might work differently.

abkrim's avatar
Level 13

@Tray2 Hello

The messages don't tell me about a server-side issue. Tell me about a strange Livewire or AlpineJS magazine.

The message in the development console tells me that despite supposedly working on a route with get, and a Livewire component, which should make a GET call, JS is making a POST call, causing the error since that route is not there authorized to that method.

That is the nature of my post, to understand why.

I am a system administrator, and my knowledge includes some programming, but it is not my primary job. Obviously, before posting I review my work, but in this case the error is not a Server error or permissions (common), unless something other than CORS, or similar, intervenes. But even so, it strikes me that ALL of the 26 creation and editing forms, except this one, work with the same structure, except that this one has a structure of 3 components, one for each action, instead of just one.

If it was a server issue, a strange configuration, none of them would work.

You do not think the same? https://imgur.com/a/uNKwUDN

Thanks for your time.

Tray2's avatar

@abkrim According to the screenshot you tell it to do a POST request on line 35 of your code. If it's that one causing the issue then try changing it to GET.

abkrim's avatar
Level 13

@Tray2 On my code there're anyithing for POST That is the question and the problem.

  • Route -> Get
  • Component Livewire -> GET
  • Form using wire

But action, and but something, in the internal management of Livewire or AlpineJS that escapes my knowledge, mysteriously converts (I don't have the knowledge, therefore it is mysterious to me) the action in javascript in a POST method.

Read my code and show me where is POST in code..

Best regards.

NOTE

I've tried force wire form to method but also not work.

 <form wire:submit.prevent="save" method="GET">
Tray2's avatar

@abkrim In your index.js file that you show you have on line 35 a POST and from the minute information that is the problem. Try changing it to GET.

abkrim's avatar
Level 13

Hi @Tray2

Imagine having to check every time there is an app update, in which you sweat the livewire js, and your other js and you have to look for the line (in a production js) and fix it.

The reason for the post is not to "solve" the problem in any way, but to know the origin of the problem, knowing that it is an issue that affects livewire, and therefore must be treated from the livewire framework, not from a solution to the end of the process.

Thanks for your time.

Tray2's avatar

@abkrim Livewire does what you tell it to do and the problem isn't in the framework. Somewhere in your custom code you send a post when you should send a get or that you have created a get route when it should have been a post route.

abkrim's avatar
Level 13

@Tray2 Well, then it is curious that the tests work, and that it works locally, and that in production, mysteriously a request that should be POST becomes GET in javascript.

Thanks for your help.

Best regards.

abkrim's avatar
Level 13

After read @tray2 and I taken a bit of distance, check locally and effectively, livewire sends a fetch with POST on all forms.

For some question that escapes me, that form and only that one, with the peculiarity of being a livewire component with three subcomponents, reached a 405 Not Allowed condition.

So, if it wasn't hand in hand with the framework, it had to be the server, and not the code.

Indeed, mod_security

--9ac02665-F--
HTTP/1.1 405 Method Not Allowed
allow: POST
Cache-Control: no-cache, private
date: Fri, 12 Nov 2021 15:18:31 GMT
Connection: close
Content-Type: text/html; charset=UTF-8
Server: Apache

--9ac02665-H--
Message: Warning. Matched phrase ".profile" at REQUEST_FILENAME. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "124"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .profile found within REQUEST_FILENAME: /livewire/message/auth.profile.update-profile"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.2"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/255/153/126"] [tag "PCI/6.5.4"]
Message: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:anomaly_score. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "93"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.2"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"]
Message: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/RESPONSE-980-CORRELATION.conf"] [line "91"] [id "980130"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 5 - SQLI=0,XSS=0,RFI=0,LFI=5,RCE=0,PHPI=0,HTTP=0,SESS=0): individual paranoia level scores: 5, 0, 0, 0"] [ver "OWASP_CRS/3.3.2"] [tag "event-correlation"]
Apache-Error: [file "apache2_util.c"] [line 271] [level 3] [client 92.59.240.169] ModSecurity: Warning. Matched phrase ".profile" at REQUEST_FILENAME. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "124"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .profile found within REQUEST_FILENAME: /livewire/message/auth.profile.update-profile"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.2"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/255/153/126"] [tag "PCI/6.5.4"] [hostname "myhostname.production.tld"] [uri "/livewire/message/auth.profile.update-profile"] [unique_id "YY6Fx8hwm9AOspTcLdqw-QABiwA"]
Apache-Error: [file "apache2_util.c"] [line 271] [level 3] [client 92.59.240.169] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:anomaly_score. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "93"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.2"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "myhostname.production.tld"] [uri "/livewire/message/auth.profile.update-profile"] [unique_id "YY6Fx8hwm9AOspTcLdqw-QABiwA"]
Apache-Error: [file "apache2_util.c"] [line 271] [level 3] [client 92.59.240.169] ModSecurity: Warning. Operator GE matched 5 at TX:inbound_anomaly_score. [file "/etc/apache2/conf.d/modsec_vendor_configs/OWASP3/rules/RESPONSE-980-CORRELATION.conf"] [line "91"] [id "980130"] [msg "Inbound Anomaly Score Exceeded (Total Inbound Score: 5 - SQLI=0,XSS=0,RFI=0,LFI=5,RCE=0,PHPI=0,HTTP=0,SESS=0): individual paranoia level scores: 5, 0, 0, 0"] [ver "OWASP_CRS/3.3.2"] [tag "event-correlation"] [hostname "myhostname.production.tld"] [uri "/index.php"] [unique_id "YY6Fx8hwm9AOspTcLdqw-QABiwA"]
Action: Intercepted (phase 2)
Apache-Handler: proxy:unix:/opt/cpanel/ea-php80/root/usr/var/run/php-fpm/dbb7d34e7ffbdd4d1231963da10ee4a052bbbe99.sock|fcgi://myhostname.production.tld
Stopwatch: 1636730311849361 24834 (- - -)
Stopwatch2: 1636730311849361 24834; combined=3619, p1=667, p2=2680, p3=0, p4=0, p5=271, sr=114, sw=1, l=0, gc=0
Producer: ModSecurity for Apache/2.9.3 (http://www.modsecurity.org/); OWASP_CRS/3.3.2.
Server: Apache
Engine-Mode: "ENABLED"

Thanks.

Ola-Yusuf's avatar

Please how did you resolve this? It just started suddenly on one of my projects in production and is not happening in dev server

Please or to participate in this conversation.