Skip to content

Instantly share code, notes, and snippets.

@lennardv2
Last active July 29, 2023 00:02
Show Gist options
  • Save lennardv2/9feec1028289cdb3c7cb1033e87442f0 to your computer and use it in GitHub Desktop.
Save lennardv2/9feec1028289cdb3c7cb1033e87442f0 to your computer and use it in GitHub Desktop.
Make livewire components more secure by default by using these traits

Livewire Security

Make livewire components more secure by default by using these traits

This trait is used to protect public livewire properties from being changed by the user by default in frontend via Livewire JS calls. See: https://www.reddit.com/r/laravel/comments/q0qrri/livewire_extremely_insecure/

If you want to 'expose' methods and properties you must use the $callable and $mutable properties to do so. By default every property defined in $rules or rules() is considered mutable.

This trait will throw an exception if mutable and callable properties are non-existant on your component;

  • WithPropertyProtection is used to protect the properties of the component
  • WithMethodProtection is used to protect the methods of the component

Why not just make them protected or private?

By keeping them public the data will not be lost on subsequent livewire updates. They can still be initially set on a components via @livewire(). But not changed after that.

For methods the case is less clear, I just like to see one explicit list at the top of my component so I know authorization is needed there.

Example:

class YourLivewireComponent extends Component
{
    use WithProtection;
  
    public $mutable = ['myMutableProp'];
    public $callable = ['myCallableMethod'];
  
    public $myMutableProp = "foo";
    public $myImmutableProp = "bar";
  
    public function myCallableMethod() { ... }
    public function myUncallableMethod() { ... }
  
    ...
  }
Livewire.all()[0].$myImmutableProp = 'evil'

Exception:

You cannot change the value of the immutable property 'myImmutableProp'. Allow it by setting the `$mutable` array.
Livewire.all()[0].myUncallableMethod()

Exception:

Method 'myUncallableMethod' is not allowed to be called. Allow it by setting the `\$callable` array.

Hits

<?php
/**
* This trait is used to protect public livewire properties from being changed by the user by default in frontend via Livewire JS calls.
* See: https://www.reddit.com/r/laravel/comments/q0qrri/livewire_extremely_insecure/
*
* If you want to 'expose' methods and properties you must use the $callable and $mutable properties to do so.
* By default every property defined in $rules or rules() is considered mutable.
*
* This trait will throw an exception if mutable and callable properties are non-existant on your component;
*
* WithPropertyProtection is used to protect the properties of the component
* WithMethodProtection is used to protect the methods of the component
*
*/
trait WithProtection
{
// public $mutable = [];
// public $callable = [];
use WithPropertyProtection;
use WithMethodProtection;
}
<?php
trait WithPropertyProtection
{
protected function initializeWithPropertyProtection()
{
if (!method_exists($this, 'mutable') && !property_exists($this, 'mutable')) {
throw new Exception("The `\$mutable` array must be present on component '{$this->getName()}'");
}
}
public function updatingWithPropertyProtection($key, $value)
{
$mutable = $this->getMutableProps();
// Mix with defined rules
$mutable = collect($this->getRules())
->map(fn($x, $key) => $key)
->values()
->merge($mutable);
foreach($mutable as $mutableProp) {
// Make sure form.*.item works
if (str_contains($mutableProp, "*")) {
$mutableProp = str_replace('.', '\\.', $mutableProp);
$mutableProp = str_replace('*', '.*', $mutableProp);
if (preg_match("/^$mutableProp$/", $key)) {
return;
}
} else {
if ($key === $mutableProp) {
return;
}
}
}
throw new Exception("You cannot change the value of the immutable property '{$key}' on component '{$this->getName()}'. Allow it by setting the `\$mutable` array.");
}
protected function getMutableProps()
{
if (method_exists($this, 'mutable')) return $this->mutable();
if (property_exists($this, 'mutable')) return $this->mutable;
return [];
}
}
<?php
namespace Livewire;
trait WithMethodProtection
{
protected function initializeWithMethodProtection()
{
if (!method_exists($this, 'callable') && !property_exists($this, 'callable')) {
throw new Exception("The `\$callable` array must be present on component '{$this->getName()}'");
}
}
public function callMethod($method, $params = [], $captureReturnValueCallback = null)
{
$method = trim($method);
// ['$set','$sync','$toggle','$refresh']
// what does $sync do?
if (method_exists($this, $method)) {
$callable = $this->getCallableMethods();
if ($method && !in_array($method, $callable)) {
throw new Exception("Method '{$method}' is not allowed to be called on component '{$this->getName()}'. Allow it by setting the `\$callable` array.");
}
}
parent::callMethod($method, $params, $captureReturnValueCallback);
}
protected function getCallableMethods()
{
if (method_exists($this, 'callable')) return $this->callable();
if (property_exists($this, 'callable')) return $this->callable;
return [];
}
}
@camya
Copy link

camya commented Feb 8, 2022

Hi @lennardv2 , can you add the namespace Livewire; to the traits.

I just copied the Traits to vendor/livewire/livewire/src and now evervything works like expected.

@lennardv2
Copy link
Author

lennardv2 commented Feb 8, 2022

Hey @camya It's a bad idea to place them in the vendor dir. When composer updates livewire you'll loose the traits... Best copy them somewhere in your own app folder (with a corresponding namespace) and reference in your (base) components. Or make a package if your want

I have a str() helper indeed, replaced it with your code. Edit: I see str() is now part of laravel 9.

@camya
Copy link

camya commented Feb 9, 2022

I see str() is now part of laravel 9.

Yes, saw that yesterday too. 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment