Skip to content

Commit 44c6466

Browse files
committed
Add a configurable depth limit to the parser
1 parent 647e973 commit 44c6466

File tree

3 files changed

+119
-37
lines changed

3 files changed

+119
-37
lines changed

include/graphqlservice/GraphQLParse.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ struct ast
3434
bool validated = false;
3535
};
3636

37-
GRAPHQLPEG_EXPORT ast parseSchemaString(std::string_view input);
38-
GRAPHQLPEG_EXPORT ast parseSchemaFile(std::string_view filename);
37+
// By default, we want to limit the depth of nested nodes. You can override this with
38+
// another value for the depthLimit parameter in these parse functions.
39+
constexpr size_t c_defaultDepthLimit = 25;
3940

40-
GRAPHQLPEG_EXPORT ast parseString(std::string_view input);
41-
GRAPHQLPEG_EXPORT ast parseFile(std::string_view filename);
41+
GRAPHQLPEG_EXPORT ast parseSchemaString(
42+
std::string_view input, size_t depthLimit = c_defaultDepthLimit);
43+
GRAPHQLPEG_EXPORT ast parseSchemaFile(
44+
std::string_view filename, size_t depthLimit = c_defaultDepthLimit);
45+
46+
GRAPHQLPEG_EXPORT ast parseString(std::string_view input, size_t depthLimit = c_defaultDepthLimit);
47+
GRAPHQLPEG_EXPORT ast parseFile(std::string_view filename, size_t depthLimit = c_defaultDepthLimit);
4248

4349
} // namespace peg
4450

include/graphqlservice/internal/SyntaxTree.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,34 @@ class ast_node
146146
std::unique_ptr<unescaped_t> _unescaped;
147147
};
148148

149+
template <class ParseInput>
150+
class depth_limit_input : public ParseInput
151+
{
152+
public:
153+
template <typename... Args>
154+
explicit depth_limit_input(size_t depthLimit, Args&&... args) noexcept
155+
: ParseInput(std::forward<Args>(args)...)
156+
, _depthLimit(depthLimit)
157+
{
158+
}
159+
160+
size_t depthLimit() const noexcept
161+
{
162+
return _depthLimit;
163+
}
164+
165+
size_t selectionSetDepth = 0;
166+
167+
private:
168+
const size_t _depthLimit;
169+
};
170+
171+
using ast_file = depth_limit_input<file_input<>>;
172+
using ast_memory = depth_limit_input<memory_input<>>;
173+
149174
struct ast_input
150175
{
151-
std::variant<std::vector<char>, std::unique_ptr<file_input<>>, std::string_view> data;
176+
std::variant<std::vector<char>, std::unique_ptr<ast_file>, std::string_view> data;
152177
};
153178

154179
} // namespace graphql::peg

src/SyntaxTree.cpp

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <memory>
1414
#include <numeric>
1515
#include <optional>
16+
#include <sstream>
1617
#include <tuple>
1718
#include <utility>
1819

@@ -659,6 +660,56 @@ struct executable_selector<type_condition> : std::true_type
659660
{
660661
};
661662

663+
template <typename Rule>
664+
struct ast_action : nothing<Rule>
665+
{
666+
};
667+
668+
struct [[nodiscard]] depth_guard
669+
{
670+
explicit depth_guard(size_t& depth) noexcept
671+
: _depth(depth)
672+
{
673+
++_depth;
674+
}
675+
676+
~depth_guard()
677+
{
678+
--_depth;
679+
}
680+
681+
depth_guard(depth_guard&&) noexcept = delete;
682+
depth_guard(const depth_guard&) = delete;
683+
684+
depth_guard& operator=(depth_guard&&) noexcept = delete;
685+
depth_guard& operator=(const depth_guard&) = delete;
686+
687+
private:
688+
size_t& _depth;
689+
};
690+
691+
template <>
692+
struct ast_action<selection_set> : maybe_nothing
693+
{
694+
template <typename Rule, apply_mode A, rewind_mode M, template <typename...> class Action,
695+
template <typename...> class Control, typename ParseInput, typename... States>
696+
[[nodiscard]] static bool match(ParseInput& in, States&&... st)
697+
{
698+
depth_guard guard(in.selectionSetDepth);
699+
if (in.selectionSetDepth > in.depthLimit())
700+
{
701+
std::ostringstream oss;
702+
703+
oss << "Exceeded " << in.depthLimit()
704+
<< " nested depth limit for https://spec.graphql.org/October2021/#SelectionSet";
705+
706+
throw parse_error(oss.str(), in);
707+
}
708+
709+
return tao::graphqlpeg::template match<Rule, A, M, Action, Control>(in, st...);
710+
}
711+
};
712+
662713
template <typename Rule>
663714
struct ast_control : normal<Rule>
664715
{
@@ -843,7 +894,7 @@ template <>
843894
const char* ast_control<schema_document_content>::error_message =
844895
"Expected schema type https://spec.graphql.org/October2021/#Document";
845896

846-
ast parseSchemaString(std::string_view input)
897+
ast parseSchemaString(std::string_view input, size_t depthLimit)
847898
{
848899
ast result { std::make_shared<ast_input>(
849900
ast_input { std::vector<char> { input.cbegin(), input.cend() } }),
@@ -854,55 +905,55 @@ ast parseSchemaString(std::string_view input)
854905
{
855906
// Try a smaller grammar with only schema type definitions first.
856907
result.root =
857-
parse_tree::parse<schema_document, ast_node, schema_selector, nothing, ast_control>(
858-
memory_input<>(data.data(), data.size(), "GraphQL"));
908+
parse_tree::parse<schema_document, ast_node, schema_selector, ast_action, ast_control>(
909+
ast_memory(depthLimit, data.data(), data.size(), "GraphQL"s));
859910
}
860911
catch (const peg::parse_error&)
861912
{
862913
// Try again with the full document grammar so validation can handle the unexepected
863914
// executable definitions if this is a mixed document.
864915
result.root =
865-
parse_tree::parse<mixed_document, ast_node, schema_selector, nothing, ast_control>(
866-
memory_input<>(data.data(), data.size(), "GraphQL"));
916+
parse_tree::parse<mixed_document, ast_node, schema_selector, ast_action, ast_control>(
917+
ast_memory(depthLimit, data.data(), data.size(), "GraphQL"s));
867918
}
868919

869920
return result;
870921
}
871922

872-
ast parseSchemaFile(std::string_view filename)
923+
ast parseSchemaFile(std::string_view filename, size_t depthLimit)
873924
{
874925
ast result;
875926

876927
try
877928
{
878-
result.input =
879-
std::make_shared<ast_input>(ast_input { std::make_unique<file_input<>>(filename) });
929+
result.input = std::make_shared<ast_input>(
930+
ast_input { std::make_unique<ast_file>(depthLimit, filename) });
880931

881-
auto& in = *std::get<std::unique_ptr<file_input<>>>(result.input->data);
932+
auto& in = *std::get<std::unique_ptr<ast_file>>(result.input->data);
882933

883934
// Try a smaller grammar with only schema type definitions first.
884935
result.root =
885-
parse_tree::parse<schema_document, ast_node, schema_selector, nothing, ast_control>(
936+
parse_tree::parse<schema_document, ast_node, schema_selector, ast_action, ast_control>(
886937
std::move(in));
887938
}
888939
catch (const peg::parse_error&)
889940
{
890-
result.input =
891-
std::make_shared<ast_input>(ast_input { std::make_unique<file_input<>>(filename) });
941+
result.input = std::make_shared<ast_input>(
942+
ast_input { std::make_unique<ast_file>(depthLimit, filename) });
892943

893-
auto& in = *std::get<std::unique_ptr<file_input<>>>(result.input->data);
944+
auto& in = *std::get<std::unique_ptr<ast_file>>(result.input->data);
894945

895946
// Try again with the full document grammar so validation can handle the unexepected
896947
// executable definitions if this is a mixed document.
897948
result.root =
898-
parse_tree::parse<mixed_document, ast_node, schema_selector, nothing, ast_control>(
949+
parse_tree::parse<mixed_document, ast_node, schema_selector, ast_action, ast_control>(
899950
std::move(in));
900951
}
901952

902953
return result;
903954
}
904955

905-
ast parseString(std::string_view input)
956+
ast parseString(std::string_view input, size_t depthLimit)
906957
{
907958
ast result { std::make_shared<ast_input>(
908959
ast_input { std::vector<char> { input.cbegin(), input.cend() } }),
@@ -913,48 +964,48 @@ ast parseString(std::string_view input)
913964
{
914965
// Try a smaller grammar with only executable definitions first.
915966
result.root = parse_tree::
916-
parse<executable_document, ast_node, executable_selector, nothing, ast_control>(
917-
memory_input<>(data.data(), data.size(), "GraphQL"));
967+
parse<executable_document, ast_node, executable_selector, ast_action, ast_control>(
968+
ast_memory(depthLimit, data.data(), data.size(), "GraphQL"s));
918969
}
919970
catch (const peg::parse_error&)
920971
{
921972
// Try again with the full document grammar so validation can handle the unexepected type
922973
// definitions if this is a mixed document.
923-
result.root =
924-
parse_tree::parse<mixed_document, ast_node, executable_selector, nothing, ast_control>(
925-
memory_input<>(data.data(), data.size(), "GraphQL"));
974+
result.root = parse_tree::
975+
parse<mixed_document, ast_node, executable_selector, ast_action, ast_control>(
976+
ast_memory(depthLimit, data.data(), data.size(), "GraphQL"s));
926977
}
927978

928979
return result;
929980
}
930981

931-
ast parseFile(std::string_view filename)
982+
ast parseFile(std::string_view filename, size_t depthLimit)
932983
{
933984
ast result;
934985

935986
try
936987
{
937-
result.input =
938-
std::make_shared<ast_input>(ast_input { std::make_unique<file_input<>>(filename) });
988+
result.input = std::make_shared<ast_input>(
989+
ast_input { std::make_unique<ast_file>(depthLimit, filename) });
939990

940-
auto& in = *std::get<std::unique_ptr<file_input<>>>(result.input->data);
991+
auto& in = *std::get<std::unique_ptr<ast_file>>(result.input->data);
941992

942993
// Try a smaller grammar with only executable definitions first.
943994
result.root = parse_tree::
944-
parse<executable_document, ast_node, executable_selector, nothing, ast_control>(
995+
parse<executable_document, ast_node, executable_selector, ast_action, ast_control>(
945996
std::move(in));
946997
}
947998
catch (const peg::parse_error&)
948999
{
949-
result.input =
950-
std::make_shared<ast_input>(ast_input { std::make_unique<file_input<>>(filename) });
1000+
result.input = std::make_shared<ast_input>(
1001+
ast_input { std::make_unique<ast_file>(depthLimit, filename) });
9511002

952-
auto& in = *std::get<std::unique_ptr<file_input<>>>(result.input->data);
1003+
auto& in = *std::get<std::unique_ptr<ast_file>>(result.input->data);
9531004

9541005
// Try again with the full document grammar so validation can handle the unexepected type
9551006
// definitions if this is a mixed document.
956-
result.root =
957-
parse_tree::parse<mixed_document, ast_node, executable_selector, nothing, ast_control>(
1007+
result.root = parse_tree::
1008+
parse<mixed_document, ast_node, executable_selector, ast_action, ast_control>(
9581009
std::move(in));
9591010
}
9601011

@@ -976,7 +1027,7 @@ peg::ast operator"" _graphql(const char* text, size_t size)
9761027
peg::ast_node,
9771028
peg::executable_selector,
9781029
peg::nothing,
979-
peg::ast_control>(peg::memory_input<>(text, size, "GraphQL"));
1030+
peg::ast_control>(peg::memory_input<>(text, size, "GraphQL"s));
9801031
}
9811032
catch (const peg::parse_error&)
9821033
{
@@ -986,7 +1037,7 @@ peg::ast operator"" _graphql(const char* text, size_t size)
9861037
peg::ast_node,
9871038
peg::executable_selector,
9881039
peg::nothing,
989-
peg::ast_control>(peg::memory_input<>(text, size, "GraphQL"));
1040+
peg::ast_control>(peg::memory_input<>(text, size, "GraphQL"s));
9901041
}
9911042

9921043
return result;

0 commit comments

Comments
 (0)