Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit fdfb105

Browse files
committed
Fix Basic auth validation in managed HttpListener
1 parent 04f3fdf commit fdfb105

File tree

3 files changed

+72
-45
lines changed

3 files changed

+72
-45
lines changed

src/System.Net.HttpListener/src/System/Net/Managed/HttpListenerContext.Managed.cs

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.ComponentModel;
66
using System.Net.WebSockets;
77
using System.Security.Principal;
8+
using System.Text;
89
using System.Threading.Tasks;
910

1011
namespace System.Net
@@ -28,63 +29,60 @@ internal HttpListenerContext(HttpConnection connection)
2829
internal bool HaveError => ErrorMessage != null;
2930

3031
internal HttpConnection Connection => _connection;
31-
32+
3233
internal void ParseAuthentication(AuthenticationSchemes expectedSchemes)
3334
{
3435
if (expectedSchemes == AuthenticationSchemes.Anonymous)
3536
return;
3637

3738
string header = Request.Headers[HttpKnownHeaderNames.Authorization];
38-
if (header == null || header.Length < 2)
39+
if (string.IsNullOrEmpty(header))
3940
return;
4041

41-
string[] authenticationData = header.Split(new char[] { ' ' }, 2);
42-
if (string.Compare(authenticationData[0], AuthenticationTypes.Basic, true) == 0)
42+
if (IsBasicHeader(header))
4343
{
44-
_user = ParseBasicAuthentication(authenticationData[1]);
44+
_user = ParseBasicAuthentication(header.Substring(AuthenticationTypes.Basic.Length + 1));
4545
}
4646
}
4747

48-
internal IPrincipal ParseBasicAuthentication(string authData)
48+
internal IPrincipal ParseBasicAuthentication(string authData) =>
49+
TryParseBasicAuth(authData, out HttpStatusCode errorCode, out string username, out string password) ?
50+
new GenericPrincipal(new HttpListenerBasicIdentity(username, password), Array.Empty<string>()) :
51+
null;
52+
53+
internal static bool IsBasicHeader(string header) =>
54+
header.Length >= 6 &&
55+
header[5] == ' ' &&
56+
string.Compare(header, 0, AuthenticationTypes.Basic, 0, 5, StringComparison.OrdinalIgnoreCase) == 0;
57+
58+
internal static bool TryParseBasicAuth(string headerValue, out HttpStatusCode errorCode, out string username, out string password)
4959
{
60+
errorCode = HttpStatusCode.OK;
61+
username = password = null;
5062
try
5163
{
52-
// Basic AUTH Data is a formatted Base64 String
53-
string user = null;
54-
string password = null;
55-
int pos = -1;
56-
string authString = Text.Encoding.Default.GetString(Convert.FromBase64String(authData));
57-
58-
// The format is DOMAIN\username:password
59-
// Domain is optional
60-
61-
pos = authString.IndexOf(':');
62-
63-
// parse the password off the end
64-
password = authString.Substring(pos + 1);
65-
66-
// discard the password
67-
authString = authString.Substring(0, pos);
68-
69-
// check if there is a domain
70-
pos = authString.IndexOf('\\');
71-
72-
if (pos > 0)
64+
if (string.IsNullOrWhiteSpace(headerValue))
7365
{
74-
user = authString.Substring(pos);
66+
return false;
7567
}
76-
else
68+
69+
string authString = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue));
70+
int colonPos = authString.IndexOf(':');
71+
if (colonPos < 0)
7772
{
78-
user = authString;
73+
// username must be at least 1 char
74+
errorCode = HttpStatusCode.BadRequest;
75+
return false;
7976
}
8077

81-
HttpListenerBasicIdentity identity = new HttpListenerBasicIdentity(user, password);
82-
return new GenericPrincipal(identity, new string[0]);
78+
username = authString.Substring(0, colonPos);
79+
password = authString.Substring(colonPos + 1);
80+
return true;
8381
}
84-
catch (Exception)
82+
catch
8583
{
86-
// Invalid auth data is swallowed silently
87-
return null;
84+
errorCode = HttpStatusCode.InternalServerError;
85+
return false;
8886
}
8987
}
9088

src/System.Net.HttpListener/src/System/Net/Managed/ListenerAsyncResult.Managed.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,22 @@ internal void Complete(HttpListenerContext context, bool synch)
135135
authFailure = true;
136136
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
137137
}
138-
else if (context.AuthenticationSchemes == AuthenticationSchemes.Basic && context.Request.Headers["Authorization"] == null)
138+
else if (context.AuthenticationSchemes == AuthenticationSchemes.Basic)
139139
{
140-
authFailure = true;
141-
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
142-
context.Response.Headers["WWW-Authenticate"] = context.AuthenticationSchemes + " realm=\"" + context._listener.Realm + "\"";
140+
HttpStatusCode errorCode = HttpStatusCode.Unauthorized;
141+
string authHeader = context.Request.Headers["Authorization"];
142+
if (authHeader == null ||
143+
!HttpListenerContext.IsBasicHeader(authHeader) ||
144+
authHeader.Length < AuthenticationTypes.Basic.Length + 2 ||
145+
!HttpListenerContext.TryParseBasicAuth(authHeader.Substring(AuthenticationTypes.Basic.Length + 1), out errorCode, out string _, out string __))
146+
{
147+
authFailure = true;
148+
context.Response.StatusCode = (int)errorCode;
149+
if (errorCode == HttpStatusCode.Unauthorized)
150+
{
151+
context.Response.Headers["WWW-Authenticate"] = context.AuthenticationSchemes + " realm=\"" + context._listener.Realm + "\"";
152+
}
153+
}
143154
}
144155

145156
if (authFailure)

src/System.Net.HttpListener/tests/AuthenticationTests.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public async Task BasicAuthentication_ValidUsernameAndPassword_Success(Authentic
5757
}
5858

5959
[ActiveIssue(19967, TargetFrameworkMonikers.NetFramework)]
60-
[ConditionalTheory(nameof(Helpers) + "." + nameof(Helpers.IsWindowsImplementation))] // [ActiveIssue(20099, TestPlatforms.Unix)]
60+
[ConditionalTheory(nameof(PlatformDetection) + "." + nameof(PlatformDetection.IsNotOneCoreUAP))]
6161
[MemberData(nameof(BasicAuthenticationHeader_TestData))]
6262
public async Task BasicAuthentication_InvalidRequest_SendsStatusCodeClient(string header, HttpStatusCode statusCode)
6363
{
@@ -124,6 +124,18 @@ public async Task TestBasicAuthenticationWithDelegate()
124124
await ValidateValidUser();
125125
}
126126

127+
[ConditionalTheory(nameof(PlatformDetection) + "." + nameof(PlatformDetection.IsNotOneCoreUAP))]
128+
[InlineData("somename:somepassword", "somename", "somepassword")]
129+
[InlineData("somename:", "somename", "")]
130+
[InlineData(":somepassword", "", "somepassword")]
131+
[InlineData("somedomain\\somename:somepassword", "somedomain\\somename", "somepassword")]
132+
[InlineData("\\somename:somepassword", "\\somename", "somepassword")]
133+
public async Task TestBasicAuthenticationWithValidAuthStrings(string authString, string expectedName, string expectedPassword)
134+
{
135+
_listener.AuthenticationSchemes = AuthenticationSchemes.Basic;
136+
await ValidateValidUser(authString, expectedName, expectedPassword);
137+
}
138+
127139
[ConditionalFact(nameof(PlatformDetection) + "." + nameof(PlatformDetection.IsNotOneCoreUAP))]
128140
public async Task TestAnonymousAuthenticationWithDelegate()
129141
{
@@ -409,21 +421,27 @@ private async Task ValidateNullUser()
409421
}
410422
}
411423

412-
private async Task ValidateValidUser()
424+
private Task ValidateValidUser() =>
425+
ValidateValidUser(string.Format("{0}:{1}", TestUser, TestPassword), TestUser, TestPassword);
426+
427+
private async Task ValidateValidUser(string authHeader, string expectedUsername, string expectedPassword)
413428
{
414429
Task<HttpListenerContext> serverContextTask = _listener.GetContextAsync();
415430
using (HttpClient client = new HttpClient())
416431
{
417432
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
418433
Basic,
419-
Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", TestUser, TestPassword))));
434+
Convert.ToBase64String(Encoding.ASCII.GetBytes(authHeader)));
420435

421-
Task<string> clientTask = client.GetStringAsync(_factory.ListeningUrl);
436+
Task <string> clientTask = client.GetStringAsync(_factory.ListeningUrl);
422437
HttpListenerContext listenerContext = await serverContextTask;
423438

424-
Assert.Equal(TestUser, listenerContext.User.Identity.Name);
425-
Assert.True(listenerContext.User.Identity.IsAuthenticated);
439+
Assert.Equal(expectedUsername, listenerContext.User.Identity.Name);
440+
Assert.Equal(!string.IsNullOrEmpty(expectedUsername), listenerContext.User.Identity.IsAuthenticated);
426441
Assert.Equal(Basic, listenerContext.User.Identity.AuthenticationType);
442+
443+
HttpListenerBasicIdentity id = Assert.IsType<HttpListenerBasicIdentity>(listenerContext.User.Identity);
444+
Assert.Equal(expectedPassword, id.Password);
427445
}
428446
}
429447

0 commit comments

Comments
 (0)