From a7faa055f908897a9bd96201be9b6ceda3df4273 Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Sat, 31 Dec 2022 05:08:02 +0300 Subject: [PATCH] basic input sanitization w/ todos sprinkled everywhere --- src/crystal-gauntlet.cr | 1 + src/endpoints/accounts/loginAccount.cr | 2 +- src/endpoints/accounts/registerAccount.cr | 2 +- src/endpoints/levels/uploadLevel.cr | 27 ++++++++++++++++--- src/endpoints/users/getUser.cr | 7 +++-- src/endpoints/users/updateUser.cr | 8 ++++-- src/lib/accounts.cr | 2 +- src/lib/clean.cr | 32 +++++++++++++++++++++++ 8 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 src/lib/clean.cr diff --git a/src/crystal-gauntlet.cr b/src/crystal-gauntlet.cr index 5609dda..44cbd46 100644 --- a/src/crystal-gauntlet.cr +++ b/src/crystal-gauntlet.cr @@ -9,6 +9,7 @@ require "./lib/hash" require "./lib/format" require "./lib/accounts" require "./lib/gjp" +require "./lib/clean" Dotenv.load diff --git a/src/endpoints/accounts/loginAccount.cr b/src/endpoints/accounts/loginAccount.cr index 634d62f..bd984fd 100644 --- a/src/endpoints/accounts/loginAccount.cr +++ b/src/endpoints/accounts/loginAccount.cr @@ -16,7 +16,7 @@ CrystalGauntlet.endpoints["/accounts/loginGJAccount.php"] = ->(body : String): S bcrypt = Crypto::Bcrypt::Password.new(hash) if bcrypt.verify(password) - user_id = Accounts.get_user_id(username, account_id.to_s) + user_id = Accounts.get_user_id(account_id.to_s) "#{account_id},#{user_id}" else return "-12" diff --git a/src/endpoints/accounts/registerAccount.cr b/src/endpoints/accounts/registerAccount.cr index 8d153bf..425457b 100644 --- a/src/endpoints/accounts/registerAccount.cr +++ b/src/endpoints/accounts/registerAccount.cr @@ -8,7 +8,7 @@ CrystalGauntlet.endpoints["/accounts/registerGJAccount.php"] = ->(body : String) params = URI::Params.parse(body) puts params.inspect - username = params["userName"] + username = Clean.clean_special(params["userName"]) password = params["password"] email = params["email"] diff --git a/src/endpoints/levels/uploadLevel.cr b/src/endpoints/levels/uploadLevel.cr index a08fa7e..47a366d 100644 --- a/src/endpoints/levels/uploadLevel.cr +++ b/src/endpoints/levels/uploadLevel.cr @@ -10,23 +10,44 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(body : String): String { if ext_id == "-1" || !Accounts.verify_gjp(ext_id, params["gjp"]) return "-1" end - user_id = Accounts.get_user_id(params["userName"], ext_id) + user_id = Accounts.get_user_id(ext_id) song_id = params["songID"] == "0" ? params["audioTrack"] : params["songID"] description = params["levelDesc"] if params["gameVersion"].to_i >= 20 # 2.0 - description = GDBase64.decode_string description + description = Clean.clean_special_lenient(GDBase64.decode_string description) + else + description = Clean.clean_special_lenient(description) end + # todo: patch descriptions to prevent color bugs + + # todo: use 1.9 levelInfo..? + # https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/uploadGJLevel.php#L56 + + # todo: use 2.2 unlisted + + # todo: https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/uploadGJLevel.php#L53 + extraString = params["extraString"]? || "29_29_29_40_29_29_29_29_29_29_29_29_29_29_29_29" + + # todo: cap level length + # todo: cap coins + # todo: cap ldm to bool + # todo: cap twoplayer to bool + # todo: cap unlisted to bool + # todo: cap requested stars + + # todo: verify object count, coins and twoplayer (i'm sure it's possible) if DATABASE.scalar("select count(*) from levels where id = ? and user_id = ?", params["levelID"], params["accountID"]).as(Int64) > 0 # update existing level + # todo raise "not implemented" else # create new level next_id = (DATABASE.scalar("select max(id) from levels").as(Int64 | Nil) || 0) + 1 - DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, level_data, extra_data, level_info, wt1, wt2, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, params["levelName"], user_id, description, params["original"].to_i32, params["gameVersion"].to_i32, params["binaryVersion"].to_i32, params["password"] == "0" ? nil : params["password"].to_i32, params["requestedStars"].to_i32, params["unlisted"].to_i32, params["levelVersion"].to_i32, params["levelString"], params["extraString"], params["levelInfo"], params["wt"], params["wt2"], song_id.to_i32, params["levelLength"].to_i32, params["objects"].to_i32, params["coins"].to_i32, params["ldm"].to_i32, params["twoPlayer"].to_i32) + DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, level_data, extra_data, level_info, wt1, wt2, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_special(params["levelName"]), user_id, description, params["original"].to_i32, params["gameVersion"].to_i32, params["binaryVersion"].to_i32, params["password"] == "0" ? nil : params["password"].to_i32, params["requestedStars"].to_i32, params["unlisted"].to_i32, params["levelVersion"].to_i32, Clean.clean_b64(params["levelString"]), Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]), Clean.clean_number(params["wt"]), Clean.clean_number(params["wt2"]), song_id.to_i32, params["levelLength"].to_i32, params["objects"].to_i32, params["coins"].to_i32, params["ldm"].to_i32, params["twoPlayer"].to_i32) next_id.to_s end diff --git a/src/endpoints/users/getUser.cr b/src/endpoints/users/getUser.cr index 79836c0..faa4777 100644 --- a/src/endpoints/users/getUser.cr +++ b/src/endpoints/users/getUser.cr @@ -41,6 +41,7 @@ CrystalGauntlet.endpoints["/getGJUserInfo20.php"] = ->(body : String): String { spider = rs.read(Int32) explosion = rs.read(Int32) + # "1:".$user["userName"].":2:".$user["userID"].":13:".$user["coins"].":17:".$user["userCoins"].":10:".$user["color1"].":11:".$user["color2"].":3:".$user["stars"].":46:".$user["diamonds"].":4:".$user["demons"].":8:".$creatorpoints.":18:".$msgstate.":19:".$reqsstate.":50:".$commentstate.":20:".$accinfo["youtubeurl"].":21:".$user["accIcon"].":22:".$user["accShip"].":23:".$user["accBall"].":24:".$user["accBird"].":25:".$user["accDart"].":26:".$user["accRobot"].":28:".$user["accGlow"].":43:".$user["accSpider"].":47:".$user["accExplosion"].":30:".$rank.":16:".$user["extID"].":31:".$friendstate.":44:".$accinfo["twitter"].":45:".$accinfo["twitch"].":29:1:49:".$badge . $appendix; return CrystalGauntlet::Format.fmt_hash({ 1 => username, 2 => user_id, @@ -80,10 +81,8 @@ CrystalGauntlet.endpoints["/getGJUserInfo20.php"] = ->(body : String): String { # badge, todo 49 => 0 }) + else + "-1" end end - - "-1" - - # echo "1:".$user["userName"].":2:".$user["userID"].":13:".$user["coins"].":17:".$user["userCoins"].":10:".$user["color1"].":11:".$user["color2"].":3:".$user["stars"].":46:".$user["diamonds"].":4:".$user["demons"].":8:".$creatorpoints.":18:".$msgstate.":19:".$reqsstate.":50:".$commentstate.":20:".$accinfo["youtubeurl"].":21:".$user["accIcon"].":22:".$user["accShip"].":23:".$user["accBall"].":24:".$user["accBird"].":25:".$user["accDart"].":26:".$user["accRobot"].":28:".$user["accGlow"].":43:".$user["accSpider"].":47:".$user["accExplosion"].":30:".$rank.":16:".$user["extID"].":31:".$friendstate.":44:".$accinfo["twitter"].":45:".$accinfo["twitch"].":29:1:49:".$badge . $appendix; } diff --git a/src/endpoints/users/updateUser.cr b/src/endpoints/users/updateUser.cr index 60975ed..170a246 100644 --- a/src/endpoints/users/updateUser.cr +++ b/src/endpoints/users/updateUser.cr @@ -13,9 +13,13 @@ CrystalGauntlet.endpoints["/updateGJUserScore22.php"] = ->(body : String): Strin return "-1" end - user_id = Accounts.get_user_id(params["userName"], account_id) + user_id = Accounts.get_user_id(account_id) - DATABASE.exec("update users set username=?, stars=?, demons=?, coins=?, user_coins=?, diamonds=?, icon_type=?, color1=?, color2=?, cube=?, ship=?, ball=?, ufo=?, wave=?, robot=?, spider=?, explosion=?, special=?, glow=?, last_played=? where id=?", params["userName"], params["stars"], params["demons"], params["coins"], params["userCoins"], params["diamonds"], params["iconType"], params["color1"], params["color2"], params["accIcon"], params["accShip"], params["accBall"], params["accBird"], params["accDart"], params["accRobot"], params["accSpider"], params["accExplosion"], params["special"], params["accGlow"], Time.utc.to_s("%Y-%m-%d %H:%M:%S"), user_id) + # todo: prevent username change unless it's a capitalization change + # todo: keep track of stat changes to look out for leaderboard cheating & whatnot + # todo: cap out demon count at the current amount of uploaded demons? same for stars & user coins. could be expensive though + + DATABASE.exec("update users set username=?, stars=?, demons=?, coins=?, user_coins=?, diamonds=?, icon_type=?, color1=?, color2=?, cube=?, ship=?, ball=?, ufo=?, wave=?, robot=?, spider=?, explosion=?, special=?, glow=?, last_played=? where id=?", params["userName"], params["stars"].to_i32, params["demons"].to_i32, params["coins"].to_i32, params["userCoins"].to_i32, params["diamonds"].to_i32, params["iconType"].to_i32, params["color1"].to_i32, params["color2"].to_i32, params["accIcon"].to_i32, params["accShip"].to_i32, params["accBall"].to_i32, params["accBird"].to_i32, params["accDart"].to_i32, params["accRobot"].to_i32, params["accSpider"].to_i32, params["accExplosion"].to_i32, params["special"].to_i32, params["accGlow"].to_i32, Time.utc.to_s("%Y-%m-%d %H:%M:%S"), user_id) user_id.to_s } diff --git a/src/lib/accounts.cr b/src/lib/accounts.cr index a8a43ab..db0f520 100644 --- a/src/lib/accounts.cr +++ b/src/lib/accounts.cr @@ -19,7 +19,7 @@ module CrystalGauntlet::Accounts end end - def get_user_id(username : String, ext_id : String) : Int32 + def get_user_id(ext_id : String) : Int32 DATABASE.query("select id from users where udid = ? or account_id = ?", ext_id, ext_id) do |rs| if rs.move_next return rs.read(Int32) diff --git a/src/lib/clean.cr b/src/lib/clean.cr new file mode 100644 index 0000000..9f92108 --- /dev/null +++ b/src/lib/clean.cr @@ -0,0 +1,32 @@ +# utilities to prevent malicious user input +module CrystalGauntlet::Clean + extend self + + # removes commonly used chars in response formatting + def clean_special(str) + # these are just the ones commonly used in response formatting + # i'm unsure if any other ones should be added, so for the time + # being i'll just keep it as is + str.gsub(/[:\|~#\(\)\0\n]/, "") + end + + # for descriptions & similar + def clean_special_lenient(str) + str.gsub(/[\0]/, "") + end + + # only allow alphanumeric chars & space + def clean_char(str) + str.gsub(/[^A-Za-z0-9 ]/, "") + end + + # only allows numbers + def clean_number(str) + str.gsub(/[^0-9]/, "") + end + + # for b64 inputs; thoroughly cleans them + def clean_b64(str) + GDBase64.encode(GDBase64.decode_string(str)) + end +end