diff --git a/Abblix.Utils.UnitTests/SanitizedTests.cs b/Abblix.Utils.UnitTests/SanitizedTests.cs new file mode 100644 index 00000000..099144ed --- /dev/null +++ b/Abblix.Utils.UnitTests/SanitizedTests.cs @@ -0,0 +1,184 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +namespace Abblix.Utils.UnitTests; + +using Xunit; + +/// +/// Contains unit tests for the struct to ensure it correctly sanitizes input strings. +/// +public class SanitizedTests +{ + /// + /// Tests that the original string is returned when no special characters are present. + /// + [Fact] + public void ToString_ShouldReturnOriginalString_WhenNoSpecialCharacters() + { + const string input = "HelloWorld"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(input, sanitizedValue.ToString()); + } + + /// + /// Tests that control characters are removed from the string. + /// + [Fact] + public void ToString_ShouldRemoveControlCharacters() + { + const string input = "Hello\x01\x02\x03World"; + const string expected = "HelloWorld"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that newline characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceNewline() + { + const string input = "Hello\nWorld"; + const string expected = "Hello\\nWorld"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that carriage return characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceCarriageReturn() + { + const string input = "Hello\rWorld"; + const string expected = "Hello\\rWorld"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that tab characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceTab() + { + const string input = "Hello\tWorld"; + const string expected = "Hello\\tWorld"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that double quote characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceDoubleQuote() + { + const string input = "Hello\"World"; + const string expected = "Hello\\\"World"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that single quote characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceSingleQuote() + { + const string input = "Hello'World"; + const string expected = "Hello\\'World"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that backslash characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceBackslash() + { + const string input = "Hello\\World"; + const string expected = "Hello\\\\World"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that comma characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceComma() + { + const string input = "Hello,World"; + const string expected = "Hello\\,World"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that semicolon characters are replaced with their escaped representation. + /// + [Fact] + public void ToString_ShouldReplaceSemicolon() + { + const string input = "Hello;World"; + const string expected = "Hello\\;World"; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } + + /// + /// Tests that a null input returns null. + /// + [Fact] + public void ToString_ShouldHandleNullInput() + { + const string? input = null; + var sanitizedValue = new Sanitized(input); + Assert.Null(sanitizedValue.ToString()); + } + + /// + /// Tests that an empty string remains unchanged. + /// + [Fact] + public void ToString_ShouldHandleEmptyString() + { + const string input = ""; + var sanitizedValue = new Sanitized(input); + Assert.Equal(input, sanitizedValue.ToString()); + } + + /// + /// Tests that a string with only control characters is sanitized to an empty string. + /// + [Fact] + public void ToString_ShouldHandleStringWithOnlyControlCharacters() + { + const string input = "\x01\x02\x03"; + const string expected = ""; + var sanitizedValue = new Sanitized(input); + Assert.Equal(expected, sanitizedValue.ToString()); + } +} diff --git a/Abblix.Utils/Sanitized.cs b/Abblix.Utils/Sanitized.cs new file mode 100644 index 00000000..1c0f7927 --- /dev/null +++ b/Abblix.Utils/Sanitized.cs @@ -0,0 +1,105 @@ +// Abblix OIDC Server Library +// Copyright (c) Abblix LLP. All rights reserved. +// +// DISCLAIMER: This software is provided 'as-is', without any express or implied +// warranty. Use at your own risk. Abblix LLP is not liable for any damages +// arising from the use of this software. +// +// LICENSE RESTRICTIONS: This code may not be modified, copied, or redistributed +// in any form outside of the official GitHub repository at: +// https://github.com/Abblix/OIDC.Server. All development and modifications +// must occur within the official repository and are managed solely by Abblix LLP. +// +// Unauthorized use, modification, or distribution of this software is strictly +// prohibited and may be subject to legal action. +// +// For full licensing terms, please visit: +// +// https://oidc.abblix.com/license +// +// CONTACT: For license inquiries or permissions, contact Abblix LLP at +// info@abblix.com + +using System.Text; + +namespace Abblix.Utils; + +/// +/// A type that sanitizes a given string by removing control characters and escaping special characters +/// to prevent log injection attacks. +/// +public readonly record struct Sanitized +{ + /// + /// Initializes a new instance of the struct with the specified source string. + /// + /// The source string to be sanitized. + public Sanitized(string? source) + { + _source = source; + } + + private readonly string? _source; + + /// + /// Returns the sanitized string representation of the source string. + /// + /// A sanitized string with control characters removed and special characters escaped. + public override string? ToString() + { + if (string.IsNullOrEmpty(_source)) + { + return _source; + } + + StringBuilder? resultBuilder = null; + var source = _source; + + for (var i = 0; i < _source.Length; i++) + { + var c = _source[i]; + + switch (c) + { + case '\n': + ReplaceTo("\\n", ref resultBuilder, source, i); + break; + case '\r': + ReplaceTo("\\r", ref resultBuilder, source, i); + break; + case '\t': + ReplaceTo("\\t", ref resultBuilder, source, i); + break; + case '\"': + ReplaceTo("\\\"", ref resultBuilder, source, i); + break; + case '\'': + ReplaceTo("\\'", ref resultBuilder, source, i); + break; + case '\\': + ReplaceTo(@"\\", ref resultBuilder, source, i); + break; + case ',': + ReplaceTo("\\,", ref resultBuilder, source, i); + break; + case ';': + ReplaceTo("\\;", ref resultBuilder, source, i); + break; + default: + if (0x00 <= c && c <= 0x1f || c == 0x7f) + ReplaceTo(null, ref resultBuilder, source, i); + else + resultBuilder?.Append(c); + break; + } + } + + return resultBuilder != null ? resultBuilder.ToString() : _source; + } + + private void ReplaceTo(string? replacement, ref StringBuilder? resultBuilder, string source, int i) + { + resultBuilder ??= new StringBuilder(source, 0, i, source.Length + (replacement?.Length ?? 0) - 1); + resultBuilder.Append(replacement); + } +}