Back to community

Why client-side checks are dead — a practical demo

147 rep191 views1 min read

I keep seeing posts that recommend client-side checks for anything sensitive. This is a quick demo of why that's dangerous, with a concrete example anyone can reproduce in 5 minutes.

The setup

Imagine a script that does this:

RegisterNetEvent('police:requestArrest', function(targetId)
    local src = source
    -- Server check: is the requester police?
    if not isPolice(src) then return end
    -- Cuff target
    TriggerClientEvent('player:cuff', targetId)
end)

You think you're safe because of isPolice(src).

The exploit

A modded client doesn't go through requestArrest. They directly call:

TriggerServerEvent('police:requestArrest', someVictimId)

No isPolice check on the trigger, only on the receiver. Wait — that should still be caught, right? It is.

But this:

RegisterNetEvent('player:cuff', function()
    -- play cuff animation
    SetEnableHandcuffs(PlayerPedId(), true)
end)

The client emits TriggerServerEvent('police:requestArrest', source) to source of someone who is in their game session. The server still rejects it. Good.

Where it actually breaks

The dangerous version:

RegisterNetEvent('player:cuff')  -- BAD: this is callable by anyone
AddEventHandler('player:cuff', function()
    SetEnableHandcuffs(PlayerPedId(), true)
end)

A modded client calls TriggerServerEvent('player:cuff') and triggers their own server's broadcast. Or worse, a script broadcasts TriggerClientEvent('player:cuff', -1) and every player gets cuffed because the receiver event is unauthenticated.

The fix

Treat every RegisterNetEvent on the client as if a malicious server could trigger it. Always verify origin:

RegisterNetEvent('player:cuff', function()
    -- 'source' on client = the server, but we got the data, not WHO sent it
    -- so we have to trust the authority of OUR server to not broadcast bad events
    SetEnableHandcuffs(PlayerPedId(), true)
end)

There's no client-side cure here. The fix is: don't put authority logic in events that can be broadcast. Use stateBags with replicated=true and a server authority, or use server-issued tokens.

Client-side AC is dead. Long live server-side state.

20 3 commentsSign in to vote

Comments (3)

Sign in to leave a comment.
  • Spot on. The number of "secure" scripts I've audited that fall apart the moment you grep for RegisterNetEvent is depressing.

    7 points
  • Server-issued one-time tokens for sensitive actions are underrated. Pair with replay protection and you're solid.

    3 points
    • kaori

      Exactly. I'll write a follow-up on token-based action auth next week.