Skip to content

Commit d2e8bcd

Browse files
SNOW-2450415 Add crl download max size limitation (#1263)
1 parent c3c305c commit d2e8bcd

File tree

9 files changed

+156
-3
lines changed

9 files changed

+156
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- v5.1.0
55
- Added `APPLICATION_PATH` to `CLIENT_ENVIRONMENT` sent during authentication to identify the application connecting to Snowflake.
66
- Renew idle sessions in the pool if keep alive is enabled.
7+
- Added `CRLDOWNLOADMAXSIZE` connection parameter to limit the maximum size of CRL files downloaded during certificate revocation checks.
78
- AWS WIF will now also check the application config and AWS profile credential store when determining the current AWS region
89
- v5.0.0
910
- Disabled CRL checks by default.

Snowflake.Data.Tests/UnitTests/Revocation/CertificateRevocationVerifierTest.cs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,78 @@ public void TestVerifyCertificateAsErrorWhenOneOfCrlsIsNotParsable()
114114
Assert.AreEqual(CertRevocationCheckResult.CertError, result);
115115
}
116116

117+
[Test]
118+
public void TestVerifyCertificateAsErrorWhenCrlExceedsMaxSize()
119+
{
120+
// arrange
121+
var expectedCrlUrls = new[] { DigiCertCrlUrl1 };
122+
var certificate = CertificateGenerator.LoadFromFile(s_digiCertCertificatePath);
123+
var parentCertificate = CertificateGenerator.LoadFromFile(s_digiCertParentCertificatePath);
124+
var crlBytes = File.ReadAllBytes(s_digiCertCrlPath);
125+
126+
var maxSize = crlBytes.Length - 1;
127+
var config = GetHttpConfig(CertRevocationCheckMode.Enabled, maxSize);
128+
129+
var restRequester = new Mock<IRestRequester>();
130+
var mockResponse = new HttpResponseMessage(HttpStatusCode.OK)
131+
{
132+
Content = new StreamContent(new MemoryStream(crlBytes))
133+
};
134+
mockResponse.Content.Headers.ContentLength = null; // Remove Content-Length to bypass early check
135+
136+
restRequester
137+
.Setup(requester => requester.Get(
138+
It.Is<RestRequestWrapper>(wrapper =>
139+
wrapper.ToRequestMessage(HttpMethod.Get).RequestUri.AbsoluteUri == DigiCertCrlUrl1)))
140+
.Returns(mockResponse);
141+
142+
var crlRepository = new CrlRepository(config.EnableCRLInMemoryCaching, config.EnableCRLDiskCaching);
143+
var environmentOperation = new Mock<EnvironmentOperations>();
144+
var verifier = new CertificateRevocationVerifier(config, TimeProvider.Instance, restRequester.Object, CertificateCrlDistributionPointsExtractor.Instance, new CrlParser(environmentOperation.Object), crlRepository);
145+
146+
// act
147+
var result = verifier.CheckCertRevocation(certificate, expectedCrlUrls, parentCertificate);
148+
149+
// assert
150+
Assert.AreEqual(CertRevocationCheckResult.CertError, result);
151+
}
152+
153+
[Test]
154+
public void TestVerifyCertificateAsErrorWhenContentLengthHeaderExceedsMaxSize()
155+
{
156+
// arrange
157+
var expectedCrlUrls = new[] { DigiCertCrlUrl1 };
158+
var certificate = CertificateGenerator.LoadFromFile(s_digiCertCertificatePath);
159+
var parentCertificate = CertificateGenerator.LoadFromFile(s_digiCertParentCertificatePath);
160+
161+
var maxSize = 1000;
162+
var contentLengthTooLarge = maxSize + 1;
163+
var config = GetHttpConfig(CertRevocationCheckMode.Enabled, maxSize);
164+
165+
var restRequester = new Mock<IRestRequester>();
166+
var mockResponse = new HttpResponseMessage(HttpStatusCode.OK)
167+
{
168+
Content = new ByteArrayContent(Array.Empty<byte>())
169+
};
170+
mockResponse.Content.Headers.ContentLength = contentLengthTooLarge;
171+
172+
restRequester
173+
.Setup(requester => requester.Get(
174+
It.Is<RestRequestWrapper>(wrapper =>
175+
wrapper.ToRequestMessage(HttpMethod.Get).RequestUri.AbsoluteUri == DigiCertCrlUrl1)))
176+
.Returns(mockResponse);
177+
178+
var crlRepository = new CrlRepository(config.EnableCRLInMemoryCaching, config.EnableCRLDiskCaching);
179+
var environmentOperation = new Mock<EnvironmentOperations>();
180+
var verifier = new CertificateRevocationVerifier(config, TimeProvider.Instance, restRequester.Object, CertificateCrlDistributionPointsExtractor.Instance, new CrlParser(environmentOperation.Object), crlRepository);
181+
182+
// act
183+
var result = verifier.CheckCertRevocation(certificate, expectedCrlUrls, parentCertificate);
184+
185+
// assert
186+
Assert.AreEqual(CertRevocationCheckResult.CertError, result);
187+
}
188+
117189
[Test]
118190
public void TestFailWhenCrlSignatureNotMatchingParentKey()
119191
{
@@ -267,7 +339,7 @@ private static void MockErrorResponseForGet(Mock<IRestRequester> restRequester,
267339
.Throws(exceptionProvider);
268340
}
269341

270-
private HttpClientConfig GetHttpConfig(CertRevocationCheckMode checkMode = CertRevocationCheckMode.Enabled) =>
342+
private HttpClientConfig GetHttpConfig(CertRevocationCheckMode checkMode = CertRevocationCheckMode.Enabled, long crlDownloadMaxSize = 209715200) =>
271343
new HttpClientConfig(
272344
null,
273345
null,
@@ -282,6 +354,8 @@ private HttpClientConfig GetHttpConfig(CertRevocationCheckMode checkMode = CertR
282354
checkMode.ToString(),
283355
false,
284356
false,
285-
false);
357+
false,
358+
10,
359+
crlDownloadMaxSize);
286360
}
287361
}

Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,11 @@ public void TestParseCrlCheckParameters(
601601
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadTimeout=abc;", "Parameter CRLDOWNLOADTIMEOUT should have an integer value.")]
602602
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadTimeout=0;", "Parameter CRLDOWNLOADTIMEOUT should be greater than 0.")]
603603
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadTimeout=-5;", "Parameter CRLDOWNLOADTIMEOUT should be greater than 0.")]
604+
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadMaxSize=abc;", "Parameter CRLDOWNLOADMAXSIZE should have a long value.")]
605+
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadMaxSize=1.5;", "Parameter CRLDOWNLOADMAXSIZE should have a long value.")]
606+
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadMaxSize=9223372036854775808;", "Parameter CRLDOWNLOADMAXSIZE should have a long value.")]
607+
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadMaxSize=0;", "Parameter CRLDOWNLOADMAXSIZE should be greater than 0.")]
608+
[TestCase("ACCOUNT=test;USER=testUser;password=testPassword;crlDownloadMaxSize=-100;", "Parameter CRLDOWNLOADMAXSIZE should be greater than 0.")]
604609
public void TestFailOnInvalidCrlParameters(string connectionString, string expectedErrorMessage)
605610
{
606611
// act

Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,23 @@ public void TestThrowsExceptionWhenSettingConnectionLimitPropertyToNonIntegerVal
133133
Assert.IsTrue(thrown.Message.Contains(expectedErrorMessage));
134134
}
135135

136+
[Test]
137+
[TestCase(100)]
138+
[TestCase(209715200)]
139+
public void TestValidCrlDownloadMaxSize(long validMaxSize)
140+
{
141+
// arrange
142+
var connectionString = $"ACCOUNT=account;USER=test;PASSWORD=test;CRLDOWNLOADMAXSIZE={validMaxSize}";
143+
var properties = SFSessionProperties.ParseConnectionString(connectionString, new SessionPropertiesContext());
144+
145+
// act
146+
var extractedProperties = SFSessionHttpClientProperties.ExtractAndValidate(properties);
147+
var config = extractedProperties.BuildHttpClientConfig();
148+
149+
// assert
150+
Assert.AreEqual(validMaxSize, config.CrlDownloadMaxSize);
151+
}
152+
136153
[Test]
137154
public void TestBuildHttpClientConfig()
138155
{

Snowflake.Data/Core/HttpUtil.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public HttpClientConfig(
3434
bool enableCRLInMemoryCaching = true,
3535
bool allowCertificatesWithoutCrlUrl = true,
3636
int crlDownloadTimeout = 10,
37+
long crlDownloadMaxSize = 209715200,
3738
string minTlsProtocol = "TLS12",
3839
string maxTlsProtocol = "TLS13"
3940
)
@@ -53,6 +54,7 @@ public HttpClientConfig(
5354
EnableCRLInMemoryCaching = enableCRLInMemoryCaching;
5455
AllowCertificatesWithoutCrlUrl = allowCertificatesWithoutCrlUrl;
5556
CrlDownloadTimeout = crlDownloadTimeout;
57+
CrlDownloadMaxSize = crlDownloadMaxSize;
5658
MinTlsProtocol = minTlsProtocol != null ? SslProtocolsExtensions.FromString(minTlsProtocol) : SslProtocols.None;
5759
MaxTlsProtocol = minTlsProtocol != null ? SslProtocolsExtensions.FromString(maxTlsProtocol) : SslProtocols.None;
5860

@@ -73,6 +75,7 @@ public HttpClientConfig(
7375
enableCRLInMemoryCaching.ToString(),
7476
allowCertificatesWithoutCrlUrl.ToString(),
7577
crlDownloadTimeout.ToString(),
78+
crlDownloadMaxSize.ToString(),
7679
minTlsProtocol,
7780
maxTlsProtocol
7881
});
@@ -93,6 +96,7 @@ public HttpClientConfig(
9396
internal readonly bool EnableCRLInMemoryCaching;
9497
internal readonly bool AllowCertificatesWithoutCrlUrl;
9598
internal readonly int CrlDownloadTimeout;
99+
internal readonly long CrlDownloadMaxSize;
96100
internal readonly SslProtocols MinTlsProtocol;
97101
internal readonly SslProtocols MaxTlsProtocol;
98102

Snowflake.Data/Core/Revocation/CertificateRevocationVerifier.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal class CertificateRevocationVerifier
2525
private readonly CrlParser _crlParser;
2626
private readonly CrlRepository _crlRepository;
2727
private readonly TimeSpan _httpTimeout;
28+
private readonly long _crlDownloadMaxSize;
2829

2930
private static readonly ConcurrentDictionary<string, object> s_locksForCrlUrls = new ConcurrentDictionary<string, object>();
3031
private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger<CertificateRevocationVerifier>();
@@ -40,6 +41,7 @@ public CertificateRevocationVerifier(
4041
_certRevocationCheckMode = config.CertRevocationCheckMode;
4142
_allowCertificatesWithoutCrlUrl = config.AllowCertificatesWithoutCrlUrl;
4243
_httpTimeout = TimeSpan.FromSeconds(config.CrlDownloadTimeout);
44+
_crlDownloadMaxSize = config.CrlDownloadMaxSize;
4345
_timeProvider = timeProvider;
4446
_restRequester = restRequester;
4547
_crlExtractor = crlExtractor;
@@ -324,18 +326,43 @@ private Crl FetchCrl(string crlUrl)
324326
{
325327
var response = _restRequester.Get(new RestRequestWrapper(request));
326328
now = _timeProvider.UtcNow();
327-
crlBytes = response.Content?.ReadAsByteArrayAsync().Result;
329+
330+
if (response?.Content == null)
331+
{
332+
s_logger.Error($"Downloading crl from {crlUrl} failed: response or content is null");
333+
return null;
334+
}
335+
336+
if (response.Content.Headers?.ContentLength.HasValue == true)
337+
{
338+
var contentLength = response.Content.Headers.ContentLength.Value;
339+
if (contentLength > _crlDownloadMaxSize)
340+
{
341+
s_logger.Error($"CRL from {crlUrl} exceeds maximum allowed size. Content-Length: {contentLength} bytes, Max allowed: {_crlDownloadMaxSize} bytes");
342+
return null;
343+
}
344+
}
345+
346+
response.Content.LoadIntoBufferAsync(_crlDownloadMaxSize).GetAwaiter().GetResult();
347+
crlBytes = response.Content.ReadAsByteArrayAsync().Result;
348+
}
349+
catch (HttpRequestException ex)
350+
{
351+
s_logger.Error($"Error reading response from {crlUrl}: Content was too large or a network error occurred. Max allowed: {_crlDownloadMaxSize} bytes. Error: {ex.Message}");
352+
return null;
328353
}
329354
catch (Exception exception)
330355
{
331356
s_logger.Error($"Downloading crl from {crlUrl} failed", exception);
332357
return null;
333358
}
359+
334360
if (crlBytes == null || crlBytes.Length == 0)
335361
{
336362
s_logger.Error($"Downloading crl from {crlUrl} failed");
337363
return null;
338364
}
365+
339366
try
340367
{
341368
var crl = _crlParser.Parse(crlBytes, now);

Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal class SFSessionHttpClientProperties
4949
internal bool _enableCrlInMemoryCaching;
5050
internal bool _allowCertificatesWithoutCrlUrl;
5151
private int _crlDownloadTimeout;
52+
private long _crlDownloadMaxSize;
5253
internal string _minTlsProtocol;
5354
internal string _maxTlsProtocol;
5455

@@ -219,6 +220,7 @@ public HttpClientConfig BuildHttpClientConfig()
219220
_enableCrlInMemoryCaching,
220221
_allowCertificatesWithoutCrlUrl,
221222
_crlDownloadTimeout,
223+
_crlDownloadMaxSize,
222224
_minTlsProtocol,
223225
_maxTlsProtocol
224226
);
@@ -287,6 +289,7 @@ public SFSessionHttpClientProperties ExtractProperties(SFSessionProperties prope
287289
_enableCrlInMemoryCaching = Boolean.Parse(propertiesDictionary[SFSessionProperty.ENABLECRLINMEMORYCACHING]),
288290
_allowCertificatesWithoutCrlUrl = Boolean.Parse(propertiesDictionary[SFSessionProperty.ALLOWCERTIFICATESWITHOUTCRLURL]),
289291
_crlDownloadTimeout = int.Parse(propertiesDictionary[SFSessionProperty.CRLDOWNLOADTIMEOUT]),
292+
_crlDownloadMaxSize = long.Parse(propertiesDictionary[SFSessionProperty.CRLDOWNLOADMAXSIZE]),
290293
_minTlsProtocol = propertiesDictionary[SFSessionProperty.MINTLS],
291294
_maxTlsProtocol = propertiesDictionary[SFSessionProperty.MAXTLS]
292295
};

Snowflake.Data/Core/Session/SFSessionProperty.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ internal enum SFSessionProperty
144144
ALLOWCERTIFICATESWITHOUTCRLURL,
145145
[SFSessionPropertyAttr(required = false, defaultValue = "10")]
146146
CRLDOWNLOADTIMEOUT,
147+
[SFSessionPropertyAttr(required = false, defaultValue = "209715200")]
148+
CRLDOWNLOADMAXSIZE,
147149
[SFSessionPropertyAttr(required = false, defaultValue = "tls12")]
148150
MINTLS,
149151
[SFSessionPropertyAttr(required = false, defaultValue = "tls13")]
@@ -344,6 +346,7 @@ private static void ValidateCrlParameters(SFSessionProperties properties)
344346
ValidateBooleanParameter(SFSessionProperty.ENABLECRLINMEMORYCACHING, properties);
345347
ValidateBooleanParameter(SFSessionProperty.ALLOWCERTIFICATESWITHOUTCRLURL, properties);
346348
ValidatePositiveIntegerParameter(SFSessionProperty.CRLDOWNLOADTIMEOUT, properties);
349+
ValidatePositiveLongParameter(SFSessionProperty.CRLDOWNLOADMAXSIZE, properties);
347350
}
348351

349352
private static void ValidateTlsParameters(SFSessionProperties properties)
@@ -413,6 +416,24 @@ private static int ValidatePositiveIntegerParameter(SFSessionProperty property,
413416
return result;
414417
}
415418

419+
private static long ValidatePositiveLongParameter(SFSessionProperty property, SFSessionProperties properties)
420+
{
421+
var propertyString = properties[property];
422+
if (!long.TryParse(propertyString, out var result))
423+
{
424+
var exception = new SnowflakeDbException(SFError.INVALID_CONNECTION_STRING, $"Parameter {property.ToString()} should have a long value.");
425+
logger.Error(exception.Message, exception);
426+
throw exception;
427+
}
428+
if (result <= 0)
429+
{
430+
var exception = new SnowflakeDbException(SFError.INVALID_CONNECTION_STRING, $"Parameter {property.ToString()} should be greater than 0.");
431+
logger.Error(exception.Message, exception);
432+
throw exception;
433+
}
434+
return result;
435+
}
436+
416437
private static void ValidateSchemeHostPort(SFSessionProperties properties)
417438
{
418439
var uriBuilder = new UriBuilder();

0 commit comments

Comments
 (0)