Subject: fix for vulnerability CVE-2012-0049 for OpenTTD 1.0.2 - 1.0.5 (Denial of service (server) via slow read attack) From: OpenTTD developer team Origin: backport, http://vcs.openttd.org/svn/changeset/23764 Bug: http://bugs.openttd.org/task/4955 Using a slow read type attack it is possible to prevent anyone from joining a server with virtually no resources. Once downloading the map no other downloads of the map can start, so downloading really slowly will prevent others from joining. This can be further aggravated by the pause-on-join setting in which case the game is paused and the players cannot continue the game during such an attack. This attack requires that the user is not banned and passes the authorization to the server, although for many servers there is no server password and thus authorization is easy. A similar attack can be done when performing the attack during the authorization phase itself, however you will not block anyone else from joining, unless you use connection multiple times until the connection limit is reached, or stop the continuation of the game of the already joined players. This attack requires the user to be merely not banned. Note that versions before 0.6.0 are vulnerable as well. However, these versions are over five years old and not supported anymore. Therefore no patches for earlier versions are provided. Before 0.3.5 it is not possible to exploit this bug via the internet as multiplayer over internet did not exist yet. The provided patch is a simplification of the fix in 1.1.5 because that version slightly changes the network protocol to tell people they got kicked due to the (password) timeout. The attached patch does not change network compatability. The fix in trunk does change network compatability. Index: src/table/settings.h =================================================================== --- src/table/settings.h (revision 23768) +++ src/table/settings.h (working copy) @@ -611,6 +611,8 @@ SDTC_VAR(network.sync_freq, SLE_UINT16,C|S,NO, 100, 0, 100, 0, STR_NULL, NULL), SDTC_VAR(network.frame_freq, SLE_UINT8,C|S,NO, 0, 0, 100, 0, STR_NULL, NULL), SDTC_VAR(network.max_join_time, SLE_UINT16, S, NO, 500, 0, 32000, 0, STR_NULL, NULL), + SDTC_VAR(network.max_download_time, SLE_UINT16, S, NO, 1000, 0, 32000, 0, STR_NULL, NULL), + SDTC_VAR(network.max_password_time, SLE_UINT16, S, NO, 2000, 0, 32000, 0, STR_NULL, NULL), SDTC_BOOL(network.pause_on_join, S, NO, true, STR_NULL, NULL), SDTC_VAR(network.server_port, SLE_UINT16, S, NO,NETWORK_DEFAULT_PORT,0,65535,0,STR_NULL, NULL), SDTC_BOOL(network.server_advertise, S, NO, false, STR_NULL, NULL), Index: src/settings_type.h =================================================================== --- src/settings_type.h (revision 23768) +++ src/settings_type.h (working copy) @@ -125,6 +125,8 @@ uint16 sync_freq; ///< how often do we check whether we are still in-sync uint8 frame_freq; ///< how often do we send commands to the clients uint16 max_join_time; ///< maximum amount of time, in game ticks, a client may take to join + uint16 max_download_time; ///< maximum amount of time, in game ticks, a client may take to download the map + uint16 max_password_time; ///< maximum amount of time, in game ticks, a client may take to enter the password bool pause_on_join; ///< pause the game when people join uint16 server_port; ///< port the server listens on char server_name[NETWORK_NAME_LENGTH]; ///< name of the server Index: src/network/network_server.cpp =================================================================== --- src/network/network_server.cpp (revision 23768) +++ src/network/network_server.cpp (working copy) @@ -1658,18 +1658,45 @@ } else { cs->lag_test = 0; } - } else if (cs->status == STATUS_PRE_ACTIVE) { - int lag = NetworkCalculateLag(cs); + } else if (cs->status == STATUS_DONE_MAP || + cs->status == STATUS_PRE_ACTIVE) { + /* The map has been sent, so this is for loading the map and syncing up. */ + uint lag = NetworkCalculateLag(cs); if (lag > _settings_client.network.max_join_time) { IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks for him to join", cs->client_id, _settings_client.network.max_join_time); NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR); } - } else if (cs->status == STATUS_INACTIVE) { - int lag = NetworkCalculateLag(cs); + } else if (cs->status == STATUS_INACTIVE || + cs->status == STATUS_NEWGRFS_CHECK || + cs->status == STATUS_AUTHORIZED) { + /* NewGRF check and authorized states should be handled almost instantly. + * So give them some lee-way, likewise for the query with inactive. */ + uint lag = NetworkCalculateLag(cs); if (lag > 4 * DAY_TICKS) { IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks to start the joining process", cs->client_id, 4 * DAY_TICKS); NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR); } + } else if (cs->status == STATUS_MAP) { + /* Downloading the map... this is the amount of time since starting the saving. */ + uint lag = NetworkCalculateLag(cs); + if (lag > _settings_client.network.max_download_time) { + IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks for him to download the map", cs->client_id, _settings_client.network.max_download_time); + NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR); + } + } else if (cs->status == STATUS_AUTH_GAME || + cs->status == STATUS_AUTH_COMPANY) { + /* Waiting for the password. */ + uint lag = NetworkCalculateLag(cs); + if (lag > _settings_client.network.max_password_time) { + IConsolePrintF(CC_ERROR,"Client #%d is dropped because it took longer than %d ticks to enter the password", cs->client_id, _settings_client.network.max_password_time); + NetworkCloseClient(cs, NETWORK_RECV_STATUS_SERVER_ERROR); + } + } else if (cs->status == STATUS_MAP_WAIT) { + /* This is an internal state where we do not wait + * on the client to move to a different state. */ + } else { + /* Bad server/code. */ + NOT_REACHED(); } if (cs->status >= STATUS_PRE_ACTIVE) {