fix: Improve validation of Bot Tokens (#1206)
* improve bot token validation by trying to decode user id from token Try to decode the user id from the supplied bot token as a way of validating the token. If this should fail, indicate that the token is invalid. * Update the tokenutils tests to pass the new validation checks * Add test case for CheckBotTokenValidity method * lint: clean up whitespace * Add check for null or whitespace string, lint whitespace * fix userid conversion * Add hint to user to check that token is not an oauth client secret * Catch exception that can be thrown by GetString * Refactor token conversion logic into it's own testable method
This commit is contained in:
committed by
Christopher F
parent
91e0f03bfd
commit
f4b1a5f25b
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
@@ -16,6 +17,65 @@ namespace Discord
|
||||
/// </remarks>
|
||||
internal const int MinBotTokenLength = 58;
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a base 64 encoded string into a ulong value.
|
||||
/// </summary>
|
||||
/// <param name="encoded"> A base 64 encoded string containing a User Id.</param>
|
||||
/// <returns> A ulong containing the decoded value of the string, or null if the value was invalid. </returns>
|
||||
internal static ulong? DecodeBase64UserId(string encoded)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(encoded))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// decode the base64 string
|
||||
var bytes = Convert.FromBase64String(encoded);
|
||||
var idStr = Encoding.UTF8.GetString(bytes);
|
||||
// try to parse a ulong from the resulting string
|
||||
if (ulong.TryParse(idStr, out var id))
|
||||
return id;
|
||||
}
|
||||
catch (DecoderFallbackException)
|
||||
{
|
||||
// ignore exception, can be thrown by GetString
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// ignore exception, can be thrown if base64 string is invalid
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// ignore exception, can be thrown by BitConverter
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the validity of a bot token by attempting to decode a ulong userid
|
||||
/// from the bot token.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// The bot token to validate.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True if the bot token was valid, false if it was not.
|
||||
/// </returns>
|
||||
internal static bool CheckBotTokenValidity(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return false;
|
||||
|
||||
// split each component of the JWT
|
||||
var segments = message.Split('.');
|
||||
|
||||
// ensure that there are three parts
|
||||
if (segments.Length != 3)
|
||||
return false;
|
||||
// return true if the user id could be determined
|
||||
return DecodeBase64UserId(segments[0]).HasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the validity of the supplied token of a specific type.
|
||||
/// </summary>
|
||||
@@ -42,13 +102,17 @@ namespace Discord
|
||||
// this value was determined by referencing examples in the discord documentation, and by comparing with
|
||||
// pre-existing tokens
|
||||
if (token.Length < MinBotTokenLength)
|
||||
throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length.", paramName: nameof(token));
|
||||
throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length. " +
|
||||
"Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token));
|
||||
// check the validity of the bot token by decoding the ulong userid from the jwt
|
||||
if (!CheckBotTokenValidity(token))
|
||||
throw new ArgumentException(message: "The Bot token was invalid. " +
|
||||
"Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token));
|
||||
break;
|
||||
default:
|
||||
// All unrecognized TokenTypes (including User tokens) are considered to be invalid.
|
||||
throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,7 @@ namespace Discord
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")]
|
||||
// 59 char token
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")]
|
||||
[InlineData("This appears to be completely invalid, however the current validation rules are not very strict.")]
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWss")]
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")]
|
||||
public void TestBotTokenDoesNotThrowExceptions(string token)
|
||||
{
|
||||
// This example token is pulled from the Discord Docs
|
||||
@@ -99,6 +97,9 @@ namespace Discord
|
||||
[InlineData("937it3ow87i4ery69876wqire")]
|
||||
// 57 char bot token
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK")]
|
||||
[InlineData("This is an invalid token, but it passes the check for string length.")]
|
||||
// valid token, but passed in twice
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")]
|
||||
public void TestBotTokenInvalidThrowsArgumentException(string token)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => TokenUtils.ValidateToken(TokenType.Bot, token));
|
||||
@@ -124,5 +125,44 @@ namespace Discord
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the <see cref="TokenUtils.CheckBotTokenValidity(string)"/> method for expected output.
|
||||
/// </summary>
|
||||
/// <param name="token"> The Bot Token to test.</param>
|
||||
/// <param name="expected"> The expected result. </param>
|
||||
[Theory]
|
||||
// this method only checks the first part of the JWT
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4..", true)]
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK", true)]
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4. this part is invalid. this part is also invalid", true)]
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.", false)]
|
||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4", false)]
|
||||
[InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw.xxxx.xxxxx", true)]
|
||||
// should not throw an unexpected exception
|
||||
[InlineData("", false)]
|
||||
[InlineData(null, false)]
|
||||
public void TestCheckBotTokenValidity(string token, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, TokenUtils.CheckBotTokenValidity(token));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// cannot pass a ulong? as a param in InlineData, so have to have a separate param
|
||||
// indicating if a value is null
|
||||
[InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw", false, 428477944009195520)]
|
||||
// should return null w/o throwing other exceptions
|
||||
[InlineData("", true, 0)]
|
||||
[InlineData(" ", true, 0)]
|
||||
[InlineData(null, true, 0)]
|
||||
[InlineData("these chars aren't allowed @U#)*@#!)*", true, 0)]
|
||||
public void TestDecodeBase64UserId(string encodedUserId, bool isNull, ulong expectedUserId)
|
||||
{
|
||||
var result = TokenUtils.DecodeBase64UserId(encodedUserId);
|
||||
if (isNull)
|
||||
Assert.Null(result);
|
||||
else
|
||||
Assert.Equal(expectedUserId, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user