Security
Authorization, route scoping, blocking, rate limiting, file validation, and SSRF protection
Chatify implements multiple layers of security to protect user data and prevent abuse.
Authorization model
Chatify uses Laravel policies to control access to conversations and messages.
ConversationPolicy
Every conversation action is gated by a policy check:
| Action | Who can do it |
|---|---|
| View | Participants only |
| Create | Any authenticated user |
| Update | Participants only |
| Update group info | Participants with edit_info permission |
| Add participants | Participants with add_members permission |
| Remove participant | Self (leave), or participants with remove_members permission |
| Manage roles | Participants with manage_admins permission |
| Transfer ownership | Owner only |
| Delete | Owner (groups), participant (direct). Cannot delete saved messages. |
| Hide | Participants only. Cannot hide saved messages. |
| Clear | Participants only. Saved messages only. |
MessagePolicy
| Action | Who can do it |
|---|---|
| View | Participants of the message's conversation |
| Create | Participants of the conversation |
| Update (edit) | Message sender only |
| Delete | Message sender only |
| Hide | Any participant |
Participant-scoped route bindings
Chatify registers custom route model bindings that scope conversation and message lookups to the authenticated user's conversations. This means:
- A user cannot access a conversation they are not a participant of, even if they guess the UUID
- A user cannot access a message from a conversation they are not a participant of
- These checks happen at the routing layer, before the controller or policy is reached
// The conversation binding filters by participant
Route::bind('conversation', function (string $value) {
return Conversation::query()
->forUser((int) auth()->user()->getKey())
->where('id', $value)
->firstOrFail();
});Block system
Users can block each other to prevent messaging:
- Blocking is mutual -- if user A blocks user B, neither can message the other
- The default
RecipientResolverchecks the block list before allowing a direct conversation - When a block is created or removed, a
UserBlockChangedevent is broadcast to both parties - Block state is reflected in the UI immediately
API
POST /api/chatify/v1/blocks/{user} # Block a user
DELETE /api/chatify/v1/blocks/{user} # Unblock a user
GET /api/chatify/v1/blocks # List blocked usersRate limiting
Two rate limiter groups protect against abuse:
| Limiter | Default | Scope | Applied to |
|---|---|---|---|
chatify-messages | 60/minute | Per user (or IP if unauthenticated) | Message send, typing, search, forward |
chatify-uploads | 10/minute | Per user (or IP if unauthenticated) | Avatar, attachment, background, settings |
When a rate limit is exceeded, the API returns 429 Too Many Requests.
File upload validation
Attachments are validated server-side:
- Extension whitelist -- Only allowed extensions pass (configured in
chatify.attachments.allowed_imagesandchatify.attachments.allowed_files) - Size limit -- Files exceeding
chatify.attachments.max_upload_size(default 150 MB) are rejected - PHP upload errors -- The server checks for PHP upload errors (
UPLOAD_ERR_INI_SIZE,UPLOAD_ERR_FORM_SIZE, etc.) and returns appropriate error messages - Storage disk -- Files are stored on the configured Laravel filesystem disk, not in publicly-guessable paths
Link preview SSRF protection
The link preview endpoint (GET /api/chatify/v1/link-preview) fetches metadata from external URLs. To prevent Server-Side Request Forgery (SSRF):
- Requests to
localhost,127.0.0.1,::1, and private IP ranges (10.x.x.x,172.16.x.x,192.168.x.x) are blocked - Only HTTP and HTTPS schemes are allowed
- The response is limited to Open Graph metadata (title, description, image URL)
Input sanitization
Message bodies are sanitized to prevent XSS and injection attacks. The frontend renders message content safely using Vue's text rendering (not v-html).
Broadcast channel authorization
Broadcast channels are protected by authorization callbacks:
- Conversation channel (
chatify.conversation.{id}): Only participants can subscribe - User channel (
chatify.user.{userId}): Only the user themselves can subscribe
Broadcast::channel('chatify.conversation.{conversationId}', function ($user, $conversationId) {
return ConversationParticipant::query()
->where('conversation_id', $conversationId)
->where('user_id', $user->getKey())
->exists();
});
Broadcast::channel('chatify.user.{userId}', function ($user, $userId) {
return (int) $user->getKey() === (int) $userId;
});Reporting vulnerabilities
If you discover a security vulnerability, please report it responsibly via the GitHub repository's security policy. Do not open a public issue for security vulnerabilities.
- GitHub Security Policy
- GitHub Issues (for non-security bugs)