# 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 ``` ### 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: `` - 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
{user_input}
{html_content|raw}
``` ### 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