SCREECHVR V18: PHOTON RPC + GAMEPLAY SECURITY
Date: 2026-10-30
Scope: Read-only review of game-owned Unity networking scripts and official
Photon PUN documentation. No code or production changes. Audit covered custom
gameplay scripts; vendor SDK/demo code and every scene prefab were not exhaustively
audited. PUN Cloud is relay/matchmaking, not an authoritative game server.
SUMMARY
Source confirms several RPCs accept state/actions without sender validation,
and local clients determine movement, tag outcomes, and combat hits. PUN reliably
relays client messages and enforces PhotonView ownership/serialization rules,
but ownership proves who currently controls a view, not whether reported damage,
position, or gameplay is legitimate. These are real trust-boundary limitations;
actual abuse still depends on the attacker being in the room and on the script/
scene being active. No evidence that a remote player can arbitrarily invoke
unrelated PhotonViews absent the proper view/RPC path; avoid claiming that.
1. RPC / EVENT / STATE INVENTORY
Tag and Infection
- `Assets/Scripts/Gamemode/TagPlayer.cs`: local `Update`/`TryTagWithHand` checks
local hand overlap and calls `TagManager.AttemptTag`. `[PunRPC] RPC_Infect(int
actorId)` checks actorId against the target view owner, then applies infected
visuals/freeze/speed locally. This is a useful target check but not sender
authorization. `[PunRPC] RPC_Cure()` and `RPC_SetTagger(bool)` have no
`PhotonMessageInfo` sender/owner validation and mutate client state.
- `Assets/Scripts/Gamemode/TagManager.cs`: `AttemptTag` is client logic, sends
`RPC_Infect`, `RPC_Cure`, `RPC_SetTagger` to All. No MasterClient-only
adjudication or trusted canonical state. Clients can call this logic locally;
any participant can attempt repeated legal RPC invocations on an accessible
view. RPCs are unbuffered, but unbuffered does not prevent live spam/replay.
`OnLastPlayerInfected(int actorId)` can increment win state without validating
sender/outcome; `CheckWinCondition` is unfinished; `UpdateFreezeTimers` empty.
- Confirmed: game mode outcomes are client-authoritative and RPC handlers lack
sender checks. Do not claim “self-tag always works”: `RPC_Infect` checks the
supplied target actor ID, but sender authorization and full call context are
missing. A determined room client may still invoke state changes; exact exploit
path requires runtime test.
Player state / movement
- `Assets/Scripts/PhotonVRPlayer.cs`: `OnPhotonSerializeView` replicates owner
pose/body data; view ownership selects writer but receiver does no movement
plausibility validation. `[PunRPC] MetaRPC()` takes no sender argument/check
and applies metadata. Player colour/cosmetics/username are client-controlled
presentation/custom properties, not proof of entitlement or identity.
- `Assets/Scripts/PhotonVRManager.cs`: `SetColour`, `SetCosmetics`, `SetUsername`
set local player properties. Join/create filters use client-set room `queue`
and `version`; these properties are matchmaking labels, not authorization.
No custom `OnPlayerPropertiesUpdate` / `OnRoomPropertiesUpdate` validator or
`OnMasterClientSwitched` implementation found in scanned main scripts.
- `Assets/Scripts/AIWanderFollowV2.cs` / `NetworkAi.cs`: AI serialization sends
position/follow state from PhotonView owner; this makes that client the AI
simulation authority. `AIWanderFollowV2.Update()` permits logic while in-room
on all clients, while NetworkAi gates NavMeshAgent by owner. Potential divergent
local decision state; not proof of a remote exploit without scene/runtime test.
- `Assets/Scripts/GrabbableObj.cs`: `OnPhotonSerializeView` sends rigidbody
position/rotation/velocity. Grab requests ownership; release transfers to
MasterClient. `Network_State(bool)` sends buffered `Grab_State(bool)`, which
changes state without sender validation. Buffered state persists/replays to
late joiners for that view; verify cleanup/lifetime. Ownership transfer enables
the interaction, not validation of proximity/physics.
Welding / physics toys
- `Assets/Scripts/WeldGun.cs`: previously audited selection code transfers
ownership before weld. Welding and the resulting physics are client-driven;
no trusted receiver adjudicates distance/eligibility. Existing null cleanup fix
does not make welds authoritative. Preserve legitimate ownership transfer and
weld-line rendering.
- `Assets/Scripts/ThusterController.cs` / `ThusterBoi.cs`: input is locally
owner-gated, but controller sends `ToggleRPC` buffered to all every frame while
active/inactive. `SyncStateRPC(bool)` has no sender/ownership check. Confirmed
RPC amplification and persistent buffer growth; state can be set by an RPC
sender. Rate/state transitions should replace per-frame buffered toggles.
- `Assets/Scripts/Flash_Light.cs`: held input sends toggle RPC; handler has no
sender check. A toggle is non-idempotent: duplicates invert state. Existing
sandbox audit identified held-button flood behavior.
- `Assets/Scripts/SandboxItems.cs`, `OwnItemGate.cs`: object gating/ownership
patterns exist; ownership is not a server validation of interactions.
PVP / damage / kick
- `Assets/Scripts/PVP_Gun.cs`: owner-side firing sends parameterless `RPC_Shoot`
to All; the owner then raycasts locally and calls target
`PVP_Player.Damage_(damage, photonView.ViewID)`. Hit target, ray result, damage
and headshot multiplier are determined client-side. `RPC_StartReload()` and
`RPC_ShowHitMarker(int damage)` lack sender validation; hit marker amount is
client-supplied. No receiver-side fire-rate/sequence validation found.
- `Assets/Scripts/PVP_Player.cs`: health/damage/death follows the client combat
call path; attacker ViewID is not proof of a valid hit. `PVP_ROCKET.cs`,
`BulletBehavior.cs`, `BulletTrailSync.cs`, `HitBox.cs`, `BulletManager.cs`, and
`Object_Type.cs` support client-driven projectile/collision/raycast behavior;
no authoritative hit validation found in scanned paths.
- `Assets/Scripts/DamagePlayer.cs`: client-driven target/explosion damage; earlier
sandbox audit observed damage multiplied by player count. Keep this finding
scoped to that script/path, not all damage mechanics.
- `Assets/Scripts/Spokes_KickGun.cs`: `TriggerKickServer()` and
`DeactivateKickServer()` are buffered RPCs without sender checks; they toggle
collider/visual state. Trigger overlap is not authoritative damage/kick proof.
- `Assets/Scripts/Kick_Player.cs`: local trigger exits app (`Application.Quit`)
on kick-layer collision; it is not a server-enforced kick/ban.
Events, properties, master client
- Search of the main game-owned Scripts tree found no custom
`PhotonNetwork.RaiseEvent` calls. Photon PUN events/serialization still exist
at framework level.
- No custom master-client callback or centralized MasterClient state authority
was found in scanned scripts. Thus there is no source-confirmed custom master
exploit path in this audit. In PUN, any room actor can become MasterClient
through normal migration; MasterClient is still a player client, not trusted
server compute. Do not place anti-cheat authority there.
- Player/room properties are server-relayed, but values are set by clients. They
are suitable for presentation/matchmaking only unless validated externally.
- RPC spam/replay findings: buffered thruster toggle every frame; buffered grab
state; buffered kick toggles; unbuffered tag/combat/flashlight calls can still
be repeated live. No common application-level RPC rate limit or action nonce
layer was found.
2. WHAT IS CONFIRMED VS THEORETICAL
Confirmed in source:
- Owner-reported movement and physics state are replicated without server-side
plausibility checks.
- Client calculates PVP hit/damage and invokes target damage path.
- Tag state changes use client-invoked RPCs; several handlers lack sender checks.
- Thuster RPC spam/buffering and toggle-style duplicate sensitivity.
- `PhotonView.IsMine` / ownership gates choose a client controller; they do not
prove the reported action is fair or physically possible.
- No main-script custom RaiseEvent authority layer or MasterClient security
authority was found.
Not established by source alone:
- Whether a specific arbitrary RPC call succeeds from an attacker-controlled
PhotonView/actor at runtime; target PhotonView, room membership, RPC list,
version, and scene lifecycle matter.
- Live prefab ownership options, Photon dashboard settings, room property
policies, MasterClient switching, active script instances, and exploit rates.
- That a modified client can force arbitrary damage in every mode. It can control
its own outgoing data; recipients currently lack authoritative hit adjudication
in scanned paths, but exact path must be reproduced in a test room.
3. DEFENSE THAT FITS PUN CLOUD
PUN Cloud cannot securely validate gameplay outcomes because it relays client
messages rather than running game simulation. Realistic protections without a
dedicated server:
- Add `PhotonMessageInfo info` to sensitive RPCs and validate `info.Sender` actor
against expected owner/controller and legal game state. This blocks accidental
or cross-actor calls, not a modified legitimate owner lying about its own play.
- For state-setting RPCs, send desired state (set active true/false) rather than
toggle; make handlers idempotent. Reject invalid transitions, stale sequence
numbers, wrong target view/actor, and impossible parameter ranges.
- Rate-limit per actor/action; avoid buffered high-frequency/transient events.
Buffer only durable state required by late joiners, and clear cached events on
despawn/reset. Keep VFX/audio separate from gameplay state.
- Tag/Infection: derive tag requests from a local hand overlap, send requester,
target, timestamp/sequence; receivers check sender owns the tagger view, target
exists, not self/frozen/already infected, cooldown and mode. For fairness with
no trusted server, make MasterClient a consistency coordinator only, replicate
canonical round state with room properties/sequence and re-elect/reconcile on
master switch; state explicitly that this is not cheat-proof because master
is a client. Avoid granting wins from an unverified RPC.
- Movement: keep owner-simulated gorilla locomotion for responsiveness. Add
conservative remote plausibility flags/telemetry and reconciliation only for
obvious malformed/extreme values; client-supplied positions cannot prove speed
hacks and should not auto-ban.
- PVP: validate sender owns weapon, ammo/cooldown/weapon state and target bounds;
move damage to an authoritative service if competitive integrity is required.
Client-side raycast remains best-effort, not secure validation. Avoid claiming
MasterClient raycast as secure authority.
- Welding: preserve ownership transfer and freeform construction. Validate
object/view exists, ownership at action, proximity/eligibility and rate; use
deterministic/idempotent weld-state IDs. With no authoritative physics server,
accept that clients can manipulate their owned objects; isolate effects from
persistent economy/competitive outcomes.
- MasterClient: treat as orchestration/host migration. Persist essential room
state in custom room properties with expected-value/sequence checks and have
clients reconcile on `OnMasterClientSwitched`. Do not trust a MasterClient
claim for bans, currency, wins, or irreversible progression.
- For strong validation of position, collision, damage, and winners, add an
authoritative game server or migrate the relevant mode to server-authoritative
networking. This is a larger architectural choice.
4. REGRESSION TESTS
Staging/test room only:
1. Two legitimate clients: normal Tag and Infection tagging, cure/freeze, late
join, disconnect/rejoin, round reset, winner/leaderboard consistency.
2. Invoke each tag RPC from wrong actor/view, wrong target, self, duplicate,
stale sequence, and cooldown; reject invalid transitions without blocking
normal hand-contact tag.
3. Flood flashlight/thruster/kick toggles and replay buffered state; verify bounded
RPC/buffer growth and stable state for existing and late-joining clients.
4. Verify only valid owner can control each weapon/object view; test ownership
request, transfer, owner leave, MasterClient migration, and cleanup.
5. PVP: normal fire/reload/headshot/rocket works; inject impossible damage,
target, hit marker, rapid fire, and duplicate RPCs; ensure any server-side
authoritative mode rejects or flags these. In PUN-only mode document residual
client authority rather than expecting perfect prevention.
6. Movement: test normal VR locomotion, tracking loss, lag, teleport-like spikes,
packet jitter; avoid false positives. Confirm no auto-ban based on telemetry.
7. Welding: two clients grab/transfer/weld items, one item cleanup, owner leaves;
legitimate weld line/state remains consistent and stale references are pruned.
8. Sandbox interactions, all active modes, voice/room joins, and scene transitions
remain intact after any future implementation.
OFFICIAL PHOTON DOCUMENTATION
- PUN RPCs and RaiseEvent: https://doc.photonengine.com/pun/current/gameplay/rpcsandraiseevent
- PhotonView Ownership & Control:
https://doc.photonengine.com/pun/current/gameplay/ownershipandcontrol
- PUN synchronization and state / `OnPhotonSerializeView`:
https://doc.photonengine.com/pun/current/gameplay/synchronization-and-state
- Master Client and Host Migration:
https://doc.photonengine.com/pun/current/gameplay/hostmigration
No implementation. No code or production changes.
END OF REPORT