Skip to content

Commit 9753c0e

Browse files
authored
fix: Don’t leak STRIP_TRANSIENTS action (#961)
* fix(master): Don’t crash master if action or payload missing Return error instead of crashing when the master receives an undefined action or an action without a `payload` field. * fix(client): Don’t forward `STRIP_TRANSIENTS` action to transports
1 parent be72c7e commit 9753c0e

5 files changed

Lines changed: 42 additions & 5 deletions

File tree

src/client/client.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
gameEvent,
2323
patch,
2424
} from '../core/action-creators';
25+
import * as Actions from '../core/action-types';
2526
import Debug from './debug/Debug.svelte';
2627
import { error } from '../core/logger';
2728
import type { LogEntry, State, SyncInfo } from '../types';
@@ -144,7 +145,7 @@ describe('multiplayer', () => {
144145

145146
beforeAll(() => {
146147
client = Client({
147-
game: { moves: { A: () => {} } },
148+
game: { moves: { A: () => {}, Invalid: () => INVALID_MOVE } },
148149
multiplayer: SocketIO({ server: host + ':' + port }),
149150
});
150151
client.start();
@@ -173,6 +174,18 @@ describe('multiplayer', () => {
173174
expect(client.transport.onAction).toHaveBeenCalled();
174175
});
175176

177+
test('strip transients action not sent to transport', () => {
178+
jest.spyOn(client.transport, 'onAction');
179+
const state = { G: {}, ctx: { phase: '' }, plugins: {} };
180+
const filteredMetadata = [];
181+
client.store.dispatch(sync({ state, filteredMetadata } as SyncInfo));
182+
client.moves.Invalid();
183+
expect(client.transport.onAction).not.toHaveBeenCalledWith(
184+
expect.any(Object),
185+
{ type: Actions.STRIP_TRANSIENTS }
186+
);
187+
});
188+
176189
test('Sends and receives chat messages', () => {
177190
jest.spyOn(client.transport, 'onAction');
178191
client.updatePlayerID('0');

src/client/client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ type ClientAction =
4343
| ActionShape.Sync
4444
| ActionShape.Update
4545
| ActionShape.Patch;
46-
type Action = CredentialedActionShape.Any | ClientAction;
46+
type Action =
47+
| CredentialedActionShape.Any
48+
| ActionShape.StripTransients
49+
| ClientAction;
4750

4851
export interface DebugOpt {
4952
target?: HTMLElement;
@@ -287,7 +290,10 @@ export class _ClientImpl<G extends any = any> {
287290
const baseState = store.getState();
288291
const result = next(action);
289292

290-
if (!('clientOnly' in action)) {
293+
if (
294+
!('clientOnly' in action) &&
295+
action.type !== Actions.STRIP_TRANSIENTS
296+
) {
291297
this.transport.onAction(baseState, action);
292298
}
293299

src/master/master.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ describe('update', () => {
233233
]);
234234
});
235235

236+
test('missing action', async () => {
237+
const { error } = await master.onUpdate(null, 0, 'matchID', '0');
238+
expect(sendAll).not.toHaveBeenCalled();
239+
expect(error).toBe('missing action or action payload');
240+
});
241+
242+
test('missing action payload', async () => {
243+
const { error } = await master.onUpdate({}, 0, 'matchID', '0');
244+
expect(sendAll).not.toHaveBeenCalled();
245+
expect(error).toBe('missing action or action payload');
246+
});
247+
236248
test('invalid matchID', async () => {
237249
await master.onUpdate(action, 0, 'default:unknown', '1');
238250
expect(sendAll).not.toHaveBeenCalled();

src/master/master.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ export class Master {
161161
matchID: string,
162162
playerID: string
163163
): Promise<void | { error: string }> {
164+
if (!credAction || !credAction.payload) {
165+
return { error: 'missing action or action payload' };
166+
}
167+
164168
let metadata: Server.MatchData | undefined;
165169
if (StorageAPI.isSynchronous(this.storageAPI)) {
166170
({ metadata } = this.storageAPI.fetch(matchID, { metadata: true }));

src/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,9 @@ export namespace ActionShape {
431431
export type Redo = StripCredentials<CredentialedActionShape.Redo>;
432432
// Private type used only for internal error processing.
433433
// Included here to preserve type-checking of reducer inputs.
434-
type _StripTransients = ReturnType<typeof ActionCreators.stripTransients>;
434+
export type StripTransients = ReturnType<
435+
typeof ActionCreators.stripTransients
436+
>;
435437
export type Any =
436438
| MakeMove
437439
| GameEvent
@@ -443,7 +445,7 @@ export namespace ActionShape {
443445
| Undo
444446
| Redo
445447
| Plugin
446-
| _StripTransients;
448+
| StripTransients;
447449
}
448450

449451
export namespace ActionPayload {

0 commit comments

Comments
 (0)