← Back to Blog/group-system-security-fix

Group System Security Fix - Protecting Your Server from Exploits

Hello everyone, I'd like to share this as I know a lot of people are experiencing issues with the ongoing group system exploits that allow players to grief by renaming other people's squads.

These exploits exist in the SCR_PlayerControllerGroupComponent class which is attached to every player controller by default.

Understanding the Problem

The vanilla implementation has a critical flaw: it trusts the client. When a player sends a request to rename a group, the game doesn't properly verify that they actually have permission to do so.

What's Actually Happening?

In the vanilla code, when someone tries to change a group name, this RPC gets called on the server:

C++
// VANILLA CODE - VULNERABLE!
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
void RPC_AskSetCustomName(int groupID, string name, int authorID)
{
    SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
    if (!groupsManager)
        return;
    
    SCR_AIGroup group = groupsManager.FindGroup(groupID);
    if (!group)
        return;
    
    group.SetCustomName(name, authorID);  // NO CHECKS! Just does it!
}

The problem here is pretty obvious once you see it. The server just accepts whatever authorID the client sends and changes the name immediately. A malicious player can:

The vanilla code only checks if the group exists, not if the person requesting the change has any right to do so. That's the core issue we need to fix.

  • Send a legitimate-looking RPC with their own player ID
  • But specify ANY group's ID, not just their own group
  • Change groups they're not even in, or groups where they're not the leader
  • Grief other players by renaming their squads to offensive content

The Solution - Actually Verify Permissions

Here's the thing: SCR_PlayerControllerGroupComponent already has a built-in function to get the real player ID from the component owner:

C++
// This already exists in the vanilla class!
int GetPlayerID()
{
    PlayerController playerController = PlayerController.Cast(GetOwner());
    if (!playerController)
        return -1;
    
    return playerController.GetPlayerId();
}

This is crucial because the server knows which player controller owns this component. We can use this to verify the request is legitimate. The client can't fake this because it's determined server-side based on who actually owns the component instance.

Step 1: Add a Helper Function

First, let's add a simple function to check if a player is actually the group leader:

C++
protected bool IsPlayerGroupLeader(int groupID, int requestingPlayerID)
{
    SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
    if (!groupsManager)
        return false;
    
    SCR_AIGroup group = groupsManager.FindGroup(groupID);
    if (!group)
        return false;
    
    // This checks if the requesting player is actually the leader
    return group.IsPlayerLeader(requestingPlayerID);
}

This gives us a clean way to verify permissions that we can reuse across multiple RPC handlers.

Step 2: Secure the Name Change RPC

Now let's compare the before and after so you can see exactly what changes:

BEFORE - Vanilla (Vulnerable):

C++
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
void RPC_AskSetCustomName(int groupID, string name, int authorID)
{
    SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
    if (!groupsManager)
        return;
    
    SCR_AIGroup group = groupsManager.FindGroup(groupID);
    if (!group)
        return;
    
    group.SetCustomName(name, authorID);
    // No verification! Trusts whatever authorID the client sent!
}

AFTER - Secured:

C++
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
override void RPC_AskSetCustomName(int groupID, string name, int authorID)
{
    // PROTECTION 1: Verify the authorID matches who actually sent this RPC
    // GetPlayerID() returns the REAL player ID from the server's perspective
    if (GetPlayerID() != authorID)
        return;  // Someone is trying to impersonate another player - blocked
    
    // PROTECTION 2: Verify this player is actually the group leader
    if (!IsPlayerGroupLeader(groupID, authorID))
        return;  // Not the leader or not even in this group - blocked
    
    SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
    if (!groupsManager)
        return;
    
    SCR_AIGroup group = groupsManager.FindGroup(groupID);
    if (!group)
        return;
    
    // Only NOW do we actually change the name
    group.SetCustomName(name, authorID);
}

What Changed and Why It Matters

Protection Layer 1: if (GetPlayerID() != authorID)

This is the first line of defense. GetPlayerID() gets the real player ID from the server - it can't be faked because it's based on who actually owns the component. The authorID is what the client claims to be. If these don't match, someone is lying and we deny the request immediately.

Protection Layer 2: if (!IsPlayerGroupLeader(groupID, authorID))

Even if the player ID is legitimate, we still need to verify they actually have permission to rename this specific group. This checks the actual group data on the server to confirm they're the leader. If they're not the leader of that group, they don't get to rename it. Simple as that.

Step 3: Apply the Same Fix to Descriptions

The description RPC has the exact same vulnerability, so we fix it the same way:

BEFORE - Vanilla (Vulnerable):

C++
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
void RPC_AskSetCustomDescription(int groupID, string desc, int authorID)
{
    SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
    if (!groupsManager)
        return;
    
    SCR_AIGroup group = groupsManager.FindGroup(groupID);
    if (!group)
        return;
    
    group.SetCustomDescription(desc, authorID);
    // Same problem - no checks!
}

AFTER - Secured:

C++
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
override void RPC_AskSetCustomDescription(int groupID, string desc, int authorID)
{
    // Same two-layer protection
    if (GetPlayerID() != authorID)
        return;
    
    if (!IsPlayerGroupLeader(groupID, authorID))
        return;
    
    SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
    if (!groupsManager)
        return;
    
    SCR_AIGroup group = groupsManager.FindGroup(groupID);
    if (!group)
        return;
    
    group.SetCustomDescription(desc, authorID);
}

Step 4: Other Vulnerable RPCs

Looking through the code, the same exploit pattern exists in several other RPC functions that should also be secured with the same verification logic:

Apply the same two-layer protection to all of them. The pattern is consistent across all these functions.

  • RPC_AskSetGroupFlag - Changing group icons/flags
  • RPC_AskSetFrequency - Changing radio frequencies
  • RPC_AskSetGroupMaxMembers - Changing group size limits

Complete Implementation

Here's the full modded class you need to create. Just save this as a .c file in your mod:

C++
modded class SCR_PlayerControllerGroupComponent
{
    // Helper function to verify group leadership
    protected bool IsPlayerGroupLeader(int groupID, int requestingPlayerID)
    {
        SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
        if (!groupsManager)
            return false;
        
        SCR_AIGroup group = groupsManager.FindGroup(groupID);
        if (!group)
            return false;
        
        return group.IsPlayerLeader(requestingPlayerID);
    }
    
    // Secured name change
    [RplRpc(RplChannel.Reliable, RplRcver.Server)]
    override void RPC_AskSetCustomName(int groupID, string name, int authorID)
    {
        if (GetPlayerID() != authorID)
            return;
        
        if (!IsPlayerGroupLeader(groupID, authorID))
            return;
        
        SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
        if (!groupsManager)
            return;
        
        SCR_AIGroup group = groupsManager.FindGroup(groupID);
        if (!group)
            return;
        
        group.SetCustomName(name, authorID);
    }
    
    // Secured description change
    [RplRpc(RplChannel.Reliable, RplRcver.Server)]
    override void RPC_AskSetCustomDescription(int groupID, string desc, int authorID)
    {
        if (GetPlayerID() != authorID)
            return;
        
        if (!IsPlayerGroupLeader(groupID, authorID))
            return;
        
        SCR_GroupsManagerComponent groupsManager = SCR_GroupsManagerComponent.GetInstance();
        if (!groupsManager)
            return;
        
        SCR_AIGroup group = groupsManager.FindGroup(groupID);
        if (!group)
            return;
        
        group.SetCustomDescription(desc, authorID);
    }
    
    // You should add similar overrides for:
    // - RPC_AskSetGroupFlag
    // - RPC_AskSetFrequency
    // - RPC_AskSetGroupMaxMembers
}

Why This Works

The key principle is simple: never trust the client, always verify on the server.

Server-side verification - All checks happen on the server where players can't manipulate the data. This is fundamental to preventing exploits.

Real identity verification - We use GetPlayerID() which gets the actual owner of the component. The client can't fake this value because it's determined by the server based on component ownership.

Permission verification - We check actual group membership and leadership status from server data, not from what the client claims.

Defense in depth - Multiple layers of checks mean even if one fails somehow, the others catch it. Both identity and permission must be valid.

Summary

The vanilla group system trusted whatever player ID the client sent in RPC calls. By adding simple server-side verification using the component's built-in GetPlayerID() function and checking actual group leadership, we completely block these exploits.

This is a critical security fix that every server should implement until Bohemia patches it officially. The fix is straightforward and follows basic security principles that should have been there from the start.

Hope this helps keep your servers safe from griefers. Let me know if you run into any issues implementing this.

Sections