Skip to content

Commit

Permalink
Added more security policy options. (#7534)
Browse files Browse the repository at this point in the history
(cherry picked from commit 17a0b77)
  • Loading branch information
michaelstaib committed Sep 30, 2024
1 parent 9a1f448 commit f74e284
Show file tree
Hide file tree
Showing 24 changed files with 490 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static IRequestExecutorBuilder AddGraphQLServer(
if (!disableDefaultSecurity)
{
builder.AddCostAnalyzer();
builder.AddIntrospectionAllowedRule(
builder.DisableIntrospection(
(sp, _) =>
{
var environment = sp.GetService<IHostEnvironment>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Net;
using CookieCrumble;
using HotChocolate.AspNetCore.Tests.Utilities;
using HotChocolate.Transport;
using HotChocolate.Transport.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -77,6 +79,152 @@ public async Task Introspection_Request_With_Rule_Removed_Fail(string environmen
}

#endif

[Fact]
public async Task Introspection_OfType_Depth_1_BadRequest()
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQL()
.SetIntrospectionAllowedDepth(
maxAllowedOfTypeDepth: 1,
maxAllowedListRecursiveDepth: 1));

var request = new GraphQLHttpRequest(
new OperationRequest(
"""
{
__schema {
types {
ofType {
ofType {
name
}
}
}
}
}
"""),
new Uri("http://localhost:5000/graphql"));

// act
var client = new DefaultGraphQLHttpClient(server.CreateClient());
using var response = await client.SendAsync(request);

// assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task Introspection_OfType_Depth_1_Depth_Analysis_Disabled()
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQL()
.SetIntrospectionAllowedDepth(
maxAllowedOfTypeDepth: 1,
maxAllowedListRecursiveDepth: 1)
.Services
.AddValidation()
.ConfigureValidation(b => b.Modifiers.Add(o => o.DisableDepthRule = true)));

var request = new GraphQLHttpRequest(
new OperationRequest(
"""
{
__schema {
types {
ofType {
ofType {
name
}
}
}
}
}
"""),
new Uri("http://localhost:5000/graphql"));

// act
var client = new DefaultGraphQLHttpClient(server.CreateClient());
using var response = await client.SendAsync(request);

// assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Fact]
public async Task Introspection_Disabled_BadRequest()
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQL()
.DisableIntrospection());

var request = new GraphQLHttpRequest(
new OperationRequest(
"""
{
__schema {
types {
ofType {
ofType {
name
}
}
}
}
}
"""),
new Uri("http://localhost:5000/graphql"));

// act
var client = new DefaultGraphQLHttpClient(server.CreateClient());
using var response = await client.SendAsync(request);

// assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task Introspection_OfType_Depth_2_OK()
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQL()
.SetIntrospectionAllowedDepth(
maxAllowedOfTypeDepth: 2,
maxAllowedListRecursiveDepth: 1));

var request = new GraphQLHttpRequest(
new OperationRequest(
"""
{
__schema {
types {
ofType {
ofType {
name
}
}
}
}
}
"""),
new Uri("http://localhost:5000/graphql"));

// act
var client = new DefaultGraphQLHttpClient(server.CreateClient());
using var response = await client.SendAsync(request);

// assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

private GraphQLHttpClient GetClient(string environment, bool removeRule = false)
{
var server = CreateStarWarsServer(
Expand All @@ -85,7 +233,8 @@ private GraphQLHttpClient GetClient(string environment, bool removeRule = false)
{
if (removeRule)
{
s.AddGraphQL().RemoveIntrospectionAllowedRule();
s.AddGraphQL()
.DisableIntrospection(disable: false);
}
});
return new DefaultGraphQLHttpClient(server.CreateClient());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public static IRequestExecutorBuilder AddValidationResultAggregator<T>(
/// Specifies if depth analysis is skipped for introspection queries.
/// </param>
/// <param name="allowRequestOverrides">
/// Defines if request depth overrides are allowed on a per request basis.
/// Defines if request depth overrides are allowed on a per-request basis.
/// </param>
/// <param name="isEnabled">
/// Defines if the validation rule is enabled.
Expand Down Expand Up @@ -220,21 +220,13 @@ public static IRequestExecutorBuilder AddMaxExecutionDepthRule(
/// Adds a validation rule that only allows requests to use `__schema` or `__type`
/// if the request carries an introspection allowed flag.
/// </summary>
[Obsolete("Use `DisableIntrospection` instead.")]
public static IRequestExecutorBuilder AddIntrospectionAllowedRule(
this IRequestExecutorBuilder builder,
Func<IServiceProvider, ValidationOptions, bool>? isEnabled = null)
=> ConfigureValidation(builder, b => b.AddIntrospectionAllowedRule(isEnabled));

/// <summary>
/// Removes a validation rule that only allows requests to use `__schema` or `__type`
/// if the request carries an introspection allowed flag.
/// </summary>
public static IRequestExecutorBuilder RemoveIntrospectionAllowedRule(
this IRequestExecutorBuilder builder)
=> ConfigureValidation(builder, b => b.RemoveIntrospectionAllowedRule());
=> DisableIntrospection(builder);

/// <summary>
/// Toggle whether introspection is allow or not.
/// Toggle whether introspection is allowed or not.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
Expand All @@ -244,17 +236,49 @@ public static IRequestExecutorBuilder RemoveIntrospectionAllowedRule(
/// If `false` introspection is disallowed, except for requests
/// that carry an introspection allowed flag.
/// </param>
[Obsolete("Use `DisableIntrospection` instead.")]
public static IRequestExecutorBuilder AllowIntrospection(
this IRequestExecutorBuilder builder,
bool allow)
{
if (!allow)
{
builder.AddIntrospectionAllowedRule();
}
=> DisableIntrospection(builder, disable: !allow);

return builder;
}
/// <summary>
/// Toggle whether introspection is disabled or not.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
/// </param>
/// <param name="disable">
/// If `true` introspection is disabled, except for requests
/// that carry an introspection allowed flag.
/// If `false` introspection is enabled.
/// </param>
public static IRequestExecutorBuilder DisableIntrospection(
this IRequestExecutorBuilder builder,
bool disable = true)
=> ConfigureValidation(
builder,
b => b.ModifyValidationOptions(
o => o.DisableIntrospection = disable));

/// <summary>
/// Toggle whether introspection is disabled or not.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
/// </param>
/// <param name="disable">
/// If `true` introspection is disabled, except for requests
/// that carry an introspection allowed flag.
/// If `false` introspection is enabled.
/// </param>
public static IRequestExecutorBuilder DisableIntrospection(
this IRequestExecutorBuilder builder,
Func<IServiceProvider, ValidationOptions, bool> disable)
=> ConfigureValidation(
builder,
b => b.ModifyValidationOptions(
(s, o) => o.DisableIntrospection = disable(s, o)));

/// <summary>
/// Sets the max allowed document validation errors.
Expand Down Expand Up @@ -283,6 +307,47 @@ public static IRequestExecutorBuilder SetMaxAllowedValidationErrors(
builder,
b => b.ConfigureValidation(
c => c.Modifiers.Add(o => o.MaxAllowedErrors = maxAllowedValidationErrors)));

return builder;
}

/// <summary>
/// Sets the max allowed depth for introspection queries.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
/// </param>
/// <param name="maxAllowedOfTypeDepth">
/// The max allowed ofType depth for introspection queries.
/// </param>
/// <param name="maxAllowedListRecursiveDepth">
/// The max allowed list recursive depth for introspection queries.
/// </param>
/// <returns>
/// Returns an <see cref="IRequestExecutorBuilder"/> that can be used to chain
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="builder"/> is <c>null</c>.
/// </exception>
public static IRequestExecutorBuilder SetIntrospectionAllowedDepth(
this IRequestExecutorBuilder builder,
ushort maxAllowedOfTypeDepth,
ushort maxAllowedListRecursiveDepth)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

ConfigureValidation(
builder,
b => b.ConfigureValidation(
c => c.Modifiers.Add(o =>
{
o.MaxAllowedOfTypeDepth = maxAllowedOfTypeDepth;
o.MaxAllowedListRecursiveDepth = maxAllowedListRecursiveDepth;
})));

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if NET6_0_OR_GREATER
using System.Buffers.Text;
using System.Linq.Expressions;
using System.Text;
Expand Down Expand Up @@ -70,3 +71,4 @@ private static int EstimateIntLength(int value)
return length;
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public IDocumentValidator CreateValidator(string? schemaName = default)
{
schemaName ??= Schema.DefaultName;
var options = _configuration.GetOptions(schemaName);
return new DocumentValidator(_contextPool, options.Rules, options.ResultAggregators, options);
var rules = _configuration.GetRules(schemaName);
var aggregators = _configuration.GetResultAggregators(schemaName);
return new DocumentValidator(_contextPool, rules, aggregators, options);
}
}
Loading

0 comments on commit f74e284

Please sign in to comment.