Some checks failed
Deploy Application / deploy (push) Has been cancelled
757 lines
17 KiB
Markdown
757 lines
17 KiB
Markdown
# LiveComponents Security Guide
|
|
|
|
**Complete security guide for LiveComponents covering CSRF protection, rate limiting, idempotency, authorization, and best practices.**
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [CSRF Protection](#csrf-protection)
|
|
2. [Rate Limiting](#rate-limiting)
|
|
3. [Idempotency](#idempotency)
|
|
4. [Action Allow-List](#action-allow-list)
|
|
5. [Authorization](#authorization)
|
|
6. [Input Validation](#input-validation)
|
|
7. [XSS Prevention](#xss-prevention)
|
|
8. [Secure State Management](#secure-state-management)
|
|
9. [Security Checklist](#security-checklist)
|
|
|
|
---
|
|
|
|
## CSRF Protection
|
|
|
|
### How It Works
|
|
|
|
LiveComponents automatically protect all actions with CSRF tokens:
|
|
|
|
1. **Token Generation**: Unique token generated per component instance
|
|
2. **Token Validation**: Token validated on every action request
|
|
3. **Token Regeneration**: Token regenerated after each action
|
|
|
|
### Implementation
|
|
|
|
**Server-Side** (Automatic):
|
|
|
|
```php
|
|
// CSRF token is automatically generated and validated
|
|
#[Action]
|
|
public function increment(): CounterState
|
|
{
|
|
// CSRF validation happens automatically before this method is called
|
|
return $this->state->increment();
|
|
}
|
|
```
|
|
|
|
**Client-Side** (Automatic):
|
|
|
|
```html
|
|
<!-- CSRF token is automatically included in action requests -->
|
|
<button data-live-action="increment">Increment</button>
|
|
```
|
|
|
|
### Manual CSRF Token Access
|
|
|
|
If you need to access CSRF tokens manually:
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\ComponentRegistry;
|
|
|
|
$registry = container()->get(ComponentRegistry::class);
|
|
$csrfToken = $registry->generateCsrfToken($componentId);
|
|
```
|
|
|
|
### CSRF Token Format
|
|
|
|
- **Length**: 32 hexadecimal characters
|
|
- **Format**: `[a-f0-9]{32}`
|
|
- **Scope**: Per component instance (not global)
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Let the framework handle CSRF automatically
|
|
- Include CSRF meta tag in base layout: `<meta name="csrf-token" content="{csrf_token}">`
|
|
- Use HTTPS in production
|
|
|
|
❌ **DON'T**:
|
|
- Disable CSRF protection
|
|
- Share CSRF tokens between components
|
|
- Store CSRF tokens in localStorage (use session)
|
|
|
|
---
|
|
|
|
## Rate Limiting
|
|
|
|
### Overview
|
|
|
|
Rate limiting prevents abuse by limiting the number of actions per time period.
|
|
|
|
### Configuration
|
|
|
|
**Global Rate Limit** (`.env`):
|
|
```env
|
|
LIVECOMPONENT_RATE_LIMIT=60 # 60 requests per minute per component
|
|
```
|
|
|
|
**Per-Action Rate Limit**:
|
|
```php
|
|
#[Action(rateLimit: 10)] // 10 requests per minute for this action
|
|
public function expensiveOperation(): State
|
|
{
|
|
// Implementation
|
|
}
|
|
```
|
|
|
|
### Rate Limit Headers
|
|
|
|
When rate limit is exceeded, response includes:
|
|
|
|
```
|
|
HTTP/1.1 429 Too Many Requests
|
|
X-RateLimit-Limit: 60
|
|
X-RateLimit-Remaining: 0
|
|
X-RateLimit-Reset: 1640995200
|
|
Retry-After: 30
|
|
```
|
|
|
|
### Client-Side Handling
|
|
|
|
```javascript
|
|
// Automatic retry after Retry-After header
|
|
LiveComponent.executeAction('counter:demo', 'increment')
|
|
.catch(error => {
|
|
if (error.status === 429) {
|
|
const retryAfter = error.headers['retry-after'];
|
|
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Set appropriate rate limits based on action cost
|
|
- Use higher limits for read operations, lower for write operations
|
|
- Monitor rate limit violations
|
|
|
|
❌ **DON'T**:
|
|
- Set rate limits too low (hurts UX)
|
|
- Set rate limits too high (allows abuse)
|
|
- Ignore rate limit violations
|
|
|
|
---
|
|
|
|
## Idempotency
|
|
|
|
### Overview
|
|
|
|
Idempotency ensures that repeating the same action multiple times has the same effect as performing it once.
|
|
|
|
### Configuration
|
|
|
|
**Per-Action Idempotency**:
|
|
```php
|
|
#[Action(idempotencyTTL: 60)] // Idempotent for 60 seconds
|
|
public function processPayment(float $amount): State
|
|
{
|
|
// This action will return cached result if called again within 60 seconds
|
|
return $this->state->processPayment($amount);
|
|
}
|
|
```
|
|
|
|
### How It Works
|
|
|
|
1. **First Request**: Action executes, result cached with idempotency key
|
|
2. **Subsequent Requests**: Same idempotency key returns cached result
|
|
3. **After TTL**: Cache expires, action can execute again
|
|
|
|
### Idempotency Key
|
|
|
|
**Client-Side**:
|
|
```javascript
|
|
// Generate unique idempotency key
|
|
const idempotencyKey = `payment-${Date.now()}-${Math.random()}`;
|
|
|
|
LiveComponent.executeAction('payment:demo', 'processPayment', {
|
|
amount: 100.00,
|
|
idempotency_key: idempotencyKey
|
|
});
|
|
```
|
|
|
|
**Server-Side** (Automatic):
|
|
- Idempotency key extracted from request
|
|
- Key includes: component ID + action name + parameters
|
|
- Cached result returned if key matches
|
|
|
|
### Use Cases
|
|
|
|
✅ **Good for**:
|
|
- Payment processing
|
|
- Order creation
|
|
- Email sending
|
|
- External API calls
|
|
|
|
❌ **Not suitable for**:
|
|
- Incrementing counters
|
|
- Appending to lists
|
|
- Time-sensitive operations
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Use idempotency for critical operations
|
|
- Set appropriate TTL (long enough to prevent duplicates, short enough to allow retries)
|
|
- Include idempotency key in client requests
|
|
|
|
❌ **DON'T**:
|
|
- Use idempotency for operations that should execute multiple times
|
|
- Set TTL too long (prevents legitimate retries)
|
|
- Rely solely on idempotency for security
|
|
|
|
---
|
|
|
|
## Action Allow-List
|
|
|
|
### Overview
|
|
|
|
Only methods marked with `#[Action]` can be called from the client.
|
|
|
|
### Implementation
|
|
|
|
```php
|
|
#[LiveComponent('user-profile')]
|
|
final readonly class UserProfileComponent implements LiveComponentContract
|
|
{
|
|
// ✅ Can be called from client
|
|
#[Action]
|
|
public function updateProfile(array $data): State
|
|
{
|
|
return $this->state->updateProfile($data);
|
|
}
|
|
|
|
// ❌ Cannot be called from client (no #[Action] attribute)
|
|
public function internalMethod(): void
|
|
{
|
|
// This method is not exposed to the client
|
|
}
|
|
|
|
// ❌ Reserved methods cannot be actions
|
|
public function mount(): State
|
|
{
|
|
// Reserved method - cannot be called as action
|
|
}
|
|
}
|
|
```
|
|
|
|
### Reserved Methods
|
|
|
|
These methods cannot be actions:
|
|
- `mount()`
|
|
- `getRenderData()`
|
|
- `getId()`
|
|
- `getState()`
|
|
- `getData()`
|
|
- Methods starting with `_` (private convention)
|
|
|
|
### Security Benefits
|
|
|
|
- **Explicit API**: Only intended methods are callable
|
|
- **No Accidental Exposure**: Internal methods stay internal
|
|
- **Clear Intent**: `#[Action]` makes API surface explicit
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Mark all public methods that should be callable with `#[Action]`
|
|
- Keep internal methods without `#[Action]`
|
|
- Use descriptive action names
|
|
|
|
❌ **DON'T**:
|
|
- Mark internal methods with `#[Action]`
|
|
- Expose sensitive operations without proper authorization
|
|
- Use generic action names like `do()` or `execute()`
|
|
|
|
---
|
|
|
|
## Authorization
|
|
|
|
### Overview
|
|
|
|
Use `#[RequiresPermission]` to restrict actions to authorized users.
|
|
|
|
### Implementation
|
|
|
|
```php
|
|
#[LiveComponent('post-editor')]
|
|
final readonly class PostEditorComponent implements LiveComponentContract
|
|
{
|
|
#[Action]
|
|
#[RequiresPermission('posts.edit')]
|
|
public function updatePost(array $data): State
|
|
{
|
|
return $this->state->updatePost($data);
|
|
}
|
|
|
|
#[Action]
|
|
#[RequiresPermission('posts.delete')]
|
|
public function deletePost(): State
|
|
{
|
|
return $this->state->deletePost();
|
|
}
|
|
|
|
// Multiple permissions (user needs ALL)
|
|
#[Action]
|
|
#[RequiresPermission('posts.edit', 'posts.publish')]
|
|
public function publishPost(): State
|
|
{
|
|
return $this->state->publishPost();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Permission Checking
|
|
|
|
**Custom Authorization Checker**:
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Security\AuthorizationCheckerInterface;
|
|
|
|
final readonly class CustomAuthorizationChecker implements AuthorizationCheckerInterface
|
|
{
|
|
public function hasPermission(string $permission): bool
|
|
{
|
|
// Check if current user has permission
|
|
return $this->user->hasPermission($permission);
|
|
}
|
|
|
|
public function hasAllPermissions(array $permissions): bool
|
|
{
|
|
foreach ($permissions as $permission) {
|
|
if (!$this->hasPermission($permission)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Unauthorized Access
|
|
|
|
When user lacks required permission:
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": {
|
|
"code": "UNAUTHORIZED",
|
|
"message": "User does not have required permission: posts.edit"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Use authorization for sensitive operations
|
|
- Check permissions server-side (never trust client)
|
|
- Use specific permissions (not generic like 'admin')
|
|
- Log authorization failures
|
|
|
|
❌ **DON'T**:
|
|
- Rely on client-side authorization checks
|
|
- Use overly broad permissions
|
|
- Skip authorization for write operations
|
|
- Expose permission names in error messages
|
|
|
|
---
|
|
|
|
## Input Validation
|
|
|
|
### Overview
|
|
|
|
Always validate input in actions before processing.
|
|
|
|
### Implementation
|
|
|
|
```php
|
|
#[Action]
|
|
public function updateProfile(array $data): State
|
|
{
|
|
// Validate input
|
|
$errors = [];
|
|
|
|
if (empty($data['name'])) {
|
|
$errors[] = 'Name is required';
|
|
}
|
|
|
|
if (isset($data['email']) && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
|
$errors[] = 'Invalid email address';
|
|
}
|
|
|
|
if (isset($data['age']) && ($data['age'] < 0 || $data['age'] > 150)) {
|
|
$errors[] = 'Age must be between 0 and 150';
|
|
}
|
|
|
|
if (!empty($errors)) {
|
|
throw new ValidationException('Validation failed', $errors);
|
|
}
|
|
|
|
// Process valid data
|
|
return $this->state->updateProfile($data);
|
|
}
|
|
```
|
|
|
|
### Type Safety
|
|
|
|
Use type hints for automatic validation:
|
|
|
|
```php
|
|
#[Action]
|
|
public function addAmount(int $amount): State
|
|
{
|
|
// PHP automatically validates $amount is an integer
|
|
if ($amount < 0) {
|
|
throw new \InvalidArgumentException('Amount must be positive');
|
|
}
|
|
|
|
return $this->state->addAmount($amount);
|
|
}
|
|
```
|
|
|
|
### Value Objects
|
|
|
|
Use Value Objects for complex validation:
|
|
|
|
```php
|
|
#[Action]
|
|
public function updateEmail(Email $email): State
|
|
{
|
|
// Email Value Object validates format automatically
|
|
return $this->state->updateEmail($email);
|
|
}
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Validate all input in actions
|
|
- Use type hints for automatic validation
|
|
- Use Value Objects for complex data
|
|
- Return clear error messages
|
|
|
|
❌ **DON'T**:
|
|
- Trust client input
|
|
- Skip validation for "internal" actions
|
|
- Expose internal validation details
|
|
- Use generic error messages
|
|
|
|
---
|
|
|
|
## XSS Prevention
|
|
|
|
### Overview
|
|
|
|
LiveComponents automatically escape output in templates.
|
|
|
|
### Template Escaping
|
|
|
|
**Automatic Escaping**:
|
|
```html
|
|
<!-- Automatically escaped -->
|
|
<div>{user_input}</div>
|
|
|
|
<!-- Safe HTML (if needed) -->
|
|
<div>{html_content|raw}</div>
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Let the framework escape output automatically
|
|
- Use `|raw` filter only when necessary
|
|
- Validate HTML content before storing
|
|
- Use Content Security Policy (CSP)
|
|
|
|
❌ **DON'T**:
|
|
- Disable automatic escaping
|
|
- Use `|raw` with user input
|
|
- Store unvalidated HTML
|
|
- Trust client-provided HTML
|
|
|
|
---
|
|
|
|
## Secure State Management
|
|
|
|
### Overview
|
|
|
|
Component state should never contain sensitive data.
|
|
|
|
### Sensitive Data
|
|
|
|
❌ **DON'T Store**:
|
|
- Passwords
|
|
- API keys
|
|
- Credit card numbers
|
|
- Session tokens
|
|
- Private keys
|
|
|
|
✅ **DO Store**:
|
|
- User IDs (not passwords)
|
|
- Display preferences
|
|
- UI state
|
|
- Non-sensitive configuration
|
|
|
|
### State Encryption
|
|
|
|
For sensitive state (if absolutely necessary):
|
|
|
|
```php
|
|
use App\Framework\Encryption\EncryptionService;
|
|
|
|
final readonly class SecureComponent implements LiveComponentContract
|
|
{
|
|
public function __construct(
|
|
private EncryptionService $encryption
|
|
) {
|
|
}
|
|
|
|
public function storeSensitiveData(string $data): State
|
|
{
|
|
$encrypted = $this->encryption->encrypt($data);
|
|
return $this->state->withEncryptedData($encrypted);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
✅ **DO**:
|
|
- Keep state minimal
|
|
- Store only necessary data
|
|
- Use server-side storage for sensitive data
|
|
- Encrypt sensitive state if needed
|
|
|
|
❌ **DON'T**:
|
|
- Store passwords or secrets in state
|
|
- Include sensitive data in state
|
|
- Trust client-provided state
|
|
- Expose internal implementation details
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
### Component Development
|
|
|
|
- [ ] All actions marked with `#[Action]`
|
|
- [ ] No sensitive data in component state
|
|
- [ ] Input validation in all actions
|
|
- [ ] Authorization checks for sensitive operations
|
|
- [ ] Rate limiting configured appropriately
|
|
- [ ] Idempotency for critical operations
|
|
- [ ] CSRF protection enabled (automatic)
|
|
- [ ] Error messages don't expose sensitive information
|
|
|
|
### Testing
|
|
|
|
- [ ] CSRF token validation tested
|
|
- [ ] Rate limiting tested
|
|
- [ ] Authorization tested
|
|
- [ ] Input validation tested
|
|
- [ ] XSS prevention tested
|
|
- [ ] Error handling tested
|
|
|
|
### Deployment
|
|
|
|
- [ ] HTTPS enabled
|
|
- [ ] CSRF protection enabled
|
|
- [ ] Rate limits configured
|
|
- [ ] Error reporting configured
|
|
- [ ] Security headers configured
|
|
- [ ] Content Security Policy configured
|
|
|
|
---
|
|
|
|
## Common Security Pitfalls
|
|
|
|
### 1. Trusting Client State
|
|
|
|
❌ **WRONG**:
|
|
```php
|
|
#[Action]
|
|
public function updateBalance(float $newBalance): State
|
|
{
|
|
// Don't trust client-provided balance!
|
|
return $this->state->withBalance($newBalance);
|
|
}
|
|
```
|
|
|
|
✅ **CORRECT**:
|
|
```php
|
|
#[Action]
|
|
public function addToBalance(float $amount): State
|
|
{
|
|
// Calculate new balance server-side
|
|
$newBalance = $this->state->balance + $amount;
|
|
return $this->state->withBalance($newBalance);
|
|
}
|
|
```
|
|
|
|
### 2. Skipping Authorization
|
|
|
|
❌ **WRONG**:
|
|
```php
|
|
#[Action]
|
|
public function deleteUser(int $userId): State
|
|
{
|
|
// No authorization check!
|
|
$this->userService->delete($userId);
|
|
return $this->state;
|
|
}
|
|
```
|
|
|
|
✅ **CORRECT**:
|
|
```php
|
|
#[Action]
|
|
#[RequiresPermission('users.delete')]
|
|
public function deleteUser(int $userId): State
|
|
{
|
|
$this->userService->delete($userId);
|
|
return $this->state;
|
|
}
|
|
```
|
|
|
|
### 3. Exposing Sensitive Data
|
|
|
|
❌ **WRONG**:
|
|
```php
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData(
|
|
templatePath: 'user-profile',
|
|
data: [
|
|
'password' => $this->user->password, // ❌ Never expose passwords!
|
|
'api_key' => $this->user->apiKey, // ❌ Never expose API keys!
|
|
]
|
|
);
|
|
}
|
|
```
|
|
|
|
✅ **CORRECT**:
|
|
```php
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData(
|
|
templatePath: 'user-profile',
|
|
data: [
|
|
'username' => $this->user->username,
|
|
'email' => $this->user->email,
|
|
// Only include safe, display data
|
|
]
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Patterns
|
|
|
|
### Pattern 1: Secure File Upload
|
|
|
|
```php
|
|
#[LiveComponent('file-uploader')]
|
|
final readonly class FileUploaderComponent implements LiveComponentContract, SupportsFileUpload
|
|
{
|
|
public function validateUpload(UploadedFile $file): array
|
|
{
|
|
$errors = [];
|
|
|
|
// Check file type
|
|
$allowedTypes = ['image/jpeg', 'image/png'];
|
|
if (!in_array($file->getMimeType(), $allowedTypes)) {
|
|
$errors[] = 'Invalid file type';
|
|
}
|
|
|
|
// Check file size
|
|
if ($file->getSize() > 5 * 1024 * 1024) {
|
|
$errors[] = 'File too large';
|
|
}
|
|
|
|
// Check file extension
|
|
$extension = pathinfo($file->getName(), PATHINFO_EXTENSION);
|
|
$allowedExtensions = ['jpg', 'jpeg', 'png'];
|
|
if (!in_array(strtolower($extension), $allowedExtensions)) {
|
|
$errors[] = 'Invalid file extension';
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
#[Action]
|
|
#[RequiresPermission('files.upload')]
|
|
public function handleUpload(UploadedFile $file): State
|
|
{
|
|
// Additional server-side validation
|
|
$errors = $this->validateUpload($file);
|
|
if (!empty($errors)) {
|
|
throw new ValidationException('Upload validation failed', $errors);
|
|
}
|
|
|
|
// Process upload securely
|
|
$path = $this->storage->store($file);
|
|
return $this->state->withUploadedFile($path);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Secure Payment Processing
|
|
|
|
```php
|
|
#[LiveComponent('payment')]
|
|
final readonly class PaymentComponent implements LiveComponentContract
|
|
{
|
|
#[Action]
|
|
#[RequiresPermission('payments.process')]
|
|
#[Action(rateLimit: 5, idempotencyTTL: 300)] // 5 per minute, 5 min idempotency
|
|
public function processPayment(
|
|
float $amount,
|
|
string $paymentMethod,
|
|
string $idempotencyKey
|
|
): State {
|
|
// Validate amount
|
|
if ($amount <= 0 || $amount > 10000) {
|
|
throw new ValidationException('Invalid amount');
|
|
}
|
|
|
|
// Validate payment method
|
|
$allowedMethods = ['credit_card', 'paypal'];
|
|
if (!in_array($paymentMethod, $allowedMethods)) {
|
|
throw new ValidationException('Invalid payment method');
|
|
}
|
|
|
|
// Process payment (idempotent)
|
|
$transaction = $this->paymentService->process(
|
|
$amount,
|
|
$paymentMethod,
|
|
$idempotencyKey
|
|
);
|
|
|
|
return $this->state->withTransaction($transaction);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
- [Performance Guide](performance-guide-complete.md) - Optimization techniques
|
|
- [End-to-End Guide](end-to-end-guide.md) - Complete development guide
|
|
- [API Reference](api-reference-complete.md) - Complete API documentation
|
|
|
|
|
|
|
|
|
|
|