basic input sanitization w/ todos sprinkled everywhere

This commit is contained in:
Jill 2022-12-31 05:08:02 +03:00
parent 0fddc7a3e6
commit a7faa055f9
8 changed files with 69 additions and 12 deletions

View file

@ -9,6 +9,7 @@ require "./lib/hash"
require "./lib/format"
require "./lib/accounts"
require "./lib/gjp"
require "./lib/clean"
Dotenv.load

View file

@ -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"

View file

@ -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"]

View file

@ -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

View file

@ -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;
}

View file

@ -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
}

View file

@ -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)

32
src/lib/clean.cr Normal file
View file

@ -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