Skip to content

Security

Diffyne includes several security features to protect your application from common attacks and unauthorized state manipulation.

Overview

Diffyne's security model is based on:

  1. State Signing: HMAC signatures prevent state tampering
  2. Flexible Verification Modes: Balance security and usability
  3. Locked Properties: Server-controlled properties cannot be modified from client
  4. Method Whitelisting: Only explicitly marked methods can be invoked
  5. Rate Limiting: Prevents abuse and DoS attacks

State Signing

Every component state is signed with an HMAC signature using your application key. This prevents clients from tampering with state data.

How It Works

  1. Server sends state with signature:
json
{
  "id": "diffyne-abc123",
  "state": {
    "count": 5,
    "name": "John"
  },
  "signature": "a1b2c3d4e5f6..."
}
  1. Client sends state back with signature:
json
{
  "componentId": "diffyne-abc123",
  "state": {
    "count": 5,
    "name": "John"
  },
  "signature": "a1b2c3d4e5f6..."
}
  1. Server verifies signature:
    • If signature matches: Request is processed
    • If signature doesn't match: 403 Forbidden error

Security Modes

Diffyne offers flexible security configuration to balance security and usability:

What it does:

  • ✅ Verifies signatures for property updates (diff:model.live, diff:model.lazy)
  • ✅ Allows form submissions without strict signature verification

Best for: Most applications - provides security where it matters most while maintaining good UX.

Configuration:

php
// config/diffyne.php
'security' => [
    'verify_state' => 'property-updates',
],

Or via .env:

DIFFYNE_VERIFY_STATE=property-updates

2. strict

What it does:

  • ✅ Verifies signatures for ALL requests (form submissions + property updates)
  • ✅ Uses lenient verification for form submissions (if enabled)
  • ⚠️ May cause issues with form submissions if state reconstruction fails

Best for: High-security applications where you need maximum protection.

Configuration:

php
'security' => [
    'verify_state' => 'strict',
    'lenient_form_verification' => true, // Recommended
],

3. none or false

What it does:

  • ❌ Disables signature verification entirely
  • ⚠️ Not recommended for production

Best for: Development only, or when you have other security measures in place.

Configuration

php
// config/diffyne.php
'security' => [
    // Signing key (defaults to APP_KEY)
    'signing_key' => env('DIFFYNE_SIGNING_KEY'),
    
    // Verify state signature mode: 'property-updates', 'strict', or false
    'verify_state' => env('DIFFYNE_VERIFY_STATE', 'property-updates'),
    
    // Allow lenient verification for form submissions (strict mode only)
    'lenient_form_verification' => env('DIFFYNE_LENIENT_FORMS', true),
    
    // Rate limiting (requests per minute)
    'rate_limit' => env('DIFFYNE_RATE_LIMIT', 60),
],

For most applications, use this configuration:

php
'security' => [
    'verify_state' => 'property-updates',
    'lenient_form_verification' => true,
    'rate_limit' => 60,
],

This provides:

  • ✅ Security for property updates (where tampering is most likely)
  • ✅ Smooth form submission experience
  • ✅ Rate limiting to prevent abuse

Locked Properties

Properties marked with #[Locked] cannot be updated from the client. This prevents tampering with server-controlled data.

Example

php
use Diffyne\Attributes\Locked;

class PostList extends Component
{
    #[Locked]
    public array $posts = []; // Server-controlled
    
    #[Locked]
    public int $total = 0; // Server-calculated
    
    public int $page = 1; // Client can change
}

Security Benefits

  1. Prevent Data Tampering: Users can't modify server data
  2. Protect Calculations: Totals, counts, etc. are server-only
  3. Enforce Business Logic: Only server can modify critical data

Attempting to Update Locked Property

If a client tries to update a locked property:

javascript
// This will fail
fetch('/_diffyne/update', {
    method: 'POST',
    body: JSON.stringify({
        property: 'posts',
        value: [], // Trying to clear posts
        // ...
    })
});

Response: 400 Bad Request with error: "Cannot update locked property: posts"

Method Whitelisting

Only methods marked with #[Invokable] can be called from the client. This provides explicit security control.

Example

php
use Diffyne\Attributes\Invokable;

class UserForm extends Component
{
    #[Invokable]
    public function save(): void
    {
        // ✅ Can be called from client
    }
    
    public function loadData(): void
    {
        // ❌ CANNOT be called from client
    }
    
    #[Invokable]
    public function delete(): void
    {
        // ✅ Can be called from client
    }
}

Security Benefits

  1. Explicit Control: You decide what's callable
  2. Prevent Unauthorized Actions: Internal methods are protected
  3. Clear Intent: Easy to see what's public API

Attempting to Call Non-Invokable Method

javascript
// This will fail
fetch('/_diffyne/call', {
    method: 'POST',
    body: JSON.stringify({
        method: 'loadData', // Not marked #[Invokable]
        // ...
    })
});

Response: 400 Bad Request with error: "Method loadData is not invokable"

Rate Limiting

Diffyne includes built-in rate limiting to prevent abuse and DoS attacks.

Configuration

php
// config/diffyne.php
'security' => [
    'rate_limit' => env('DIFFYNE_RATE_LIMIT', 60), // requests per minute
],

How It Works

  • Each IP address is limited to N requests per minute
  • Exceeding the limit returns 429 Too Many Requests
  • Limits are per-route (update, call, etc.)

Customizing Rate Limits

You can customize rate limits in your middleware or route configuration:

php
// In your service provider or middleware
RateLimiter::for('diffyne', function (Request $request) {
    return Limit::perMinute(100)->by($request->ip());
});

Testing Security

Test 1: Tamper with State Signature

javascript
// In browser console
const component = document.querySelector('[diff\\:id]');
const componentId = component.getAttribute('diff:id');
const state = JSON.parse(component.getAttribute('diff:state'));

// Try to tamper with signature
fetch('/_diffyne/update', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        componentId: componentId,
        property: 'page',
        value: 2,
        state: state,
        signature: 'tampered_signature_12345', // ❌ Invalid signature
    })
})
.then(r => r.json())
.then(data => {
    console.log('Result:', data);
    // Expected: 403 Forbidden - "Invalid state signature"
});

Test 2: Try to Update Locked Property

javascript
const component = document.querySelector('[diff\\:id]');
const componentId = component.getAttribute('diff:id');
const state = JSON.parse(component.getAttribute('diff:state'));
const signature = component.getAttribute('diff:signature');

// Try to update locked property
fetch('/_diffyne/update', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        componentId: componentId,
        property: 'posts', // Locked property
        value: [], // Trying to clear
        state: state,
        signature: signature,
    })
})
.then(r => r.json())
.then(data => {
    console.log('Result:', data);
    // Expected: 400 Bad Request - "Cannot update locked property: posts"
});

Test 3: Try to Call Non-Invokable Method

javascript
const component = document.querySelector('[diff\\:id]');
const componentId = component.getAttribute('diff:id');
const state = JSON.parse(component.getAttribute('diff:state'));
const signature = component.getAttribute('diff:signature');

// Try to call non-invokable method
fetch('/_diffyne/call', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        componentId: componentId,
        method: 'loadData', // Not marked #[Invokable]
        params: [],
        state: state,
        signature: signature,
    })
})
.then(r => r.json())
.then(data => {
    console.log('Result:', data);
    // Expected: 400 Bad Request - "Method loadData is not invokable"
});

Best Practices

1. Always Use #[Locked] for Server Data

php
// ✅ Good
#[Locked]
public array $posts = [];

// ❌ Bad - allows tampering
public array $posts = [];

2. Mark Only Public Actions as #[Invokable]

php
// ✅ Good - public action
#[Invokable]
public function save(): void { }

// ❌ Bad - internal method
#[Invokable]
public function loadData(): void { }

3. Use Appropriate Verification Mode

bash
# ✅ Good - recommended for most apps
DIFFYNE_VERIFY_STATE=property-updates

# ✅ Good - maximum security
DIFFYNE_VERIFY_STATE=strict

# ❌ Bad - only for development
DIFFYNE_VERIFY_STATE=false

4. Set Appropriate Rate Limits

php
// ✅ Good - reasonable limit
'rate_limit' => 60, // 1 request per second

// ❌ Bad - too high (allows abuse)
'rate_limit' => 10000,

// ❌ Bad - too low (breaks UX)
'rate_limit' => 5,

5. Use Strong Signing Keys

bash
# ✅ Good - use APP_KEY (strong, random)
DIFFYNE_SIGNING_KEY=

# ❌ Bad - weak key
DIFFYNE_SIGNING_KEY=secret123

6. Validate All Input

Even with locked properties, always validate:

php
#[Invokable]
public function updateName(string $name): void
{
    // ✅ Always validate
    $validated = $this->validate([
        'name' => 'required|string|max:255',
    ]);
    
    $this->name = $validated['name'];
}

Common Security Patterns

Pattern 1: Server-Controlled Lists

php
class ProductList extends Component
{
    #[Locked]
    public array $products = []; // Server loads
    
    #[Locked]
    public int $total = 0; // Server calculates
    
    public string $search = ''; // Client can change
    
    public function updatedSearch(): void
    {
        // Server reloads products based on search
        $this->loadProducts();
    }
    
    private function loadProducts(): void
    {
        $query = Product::query();
        
        if ($this->search) {
            $query->where('name', 'like', "%{$this->search}%");
        }
        
        $this->products = $query->get()->toArray();
        $this->total = count($this->products);
    }
}

Pattern 2: Protected Actions

php
class AdminPanel extends Component
{
    #[Invokable]
    public function deleteUser(int $id): void
    {
        // ✅ Always check authorization
        if (!auth()->user()->isAdmin()) {
            abort(403);
        }
        
        User::find($id)->delete();
    }
}

Pattern 3: Validated Updates

php
class UserForm extends Component
{
    public string $email = '';
    
    #[Invokable]
    public function updateEmail(): void
    {
        // ✅ Always validate
        $validated = $this->validate([
            'email' => 'required|email|unique:users',
        ]);
        
        $this->email = $validated['email'];
    }
}

Troubleshooting

Form submissions failing with signature errors?

  1. Check your .env file:

    DIFFYNE_VERIFY_STATE=property-updates
  2. Clear config cache:

    bash
    php artisan config:clear
  3. Verify state signature is being sent correctly

Migration from strict mode

If you were experiencing issues with strict verification:

php
// Old (causing issues)
'verify_state' => true, // or 'verify_state' => env('DIFFYNE_VERIFY_STATE', true)

// New (recommended)
'verify_state' => 'property-updates', // or env('DIFFYNE_VERIFY_STATE', 'property-updates')

No code changes needed - your forms will now work smoothly while property updates remain secure.

Security Checklist

  • [ ] All server-controlled data uses #[Locked]
  • [ ] Only public actions are marked #[Invokable]
  • [ ] State verification is configured appropriately (DIFFYNE_VERIFY_STATE=property-updates recommended)
  • [ ] Rate limiting is configured appropriately
  • [ ] All user input is validated
  • [ ] Authorization checks are in place for sensitive actions
  • [ ] Signing key is strong (using APP_KEY)
  • [ ] Security testing has been performed

Next Steps

Learn more about keeping your app secure:

Released under the MIT License.