Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/core/uri/include/sourcemeta/core/uri.h
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,36 @@ class SOURCEMETA_CORE_URI_EXPORT URI {
[[nodiscard]] static auto is_uri_reference(std::string_view input) noexcept
-> bool;

/// Check if the given string is a valid RFC 3986 Section 4.2 absolute-path
/// reference, a relative reference whose relative part is a path-absolute,
/// with an optional query and fragment, without constructing a full URI
/// object.
///
/// Such a reference resolves within the origin of whatever it is resolved
/// against, since it begins with a single slash and cannot begin with two,
/// which is what a network-path reference would need to name another
/// authority. That makes it the shape a redirect target must have for the
/// redirect to stay same-origin.
///
/// The grammar is applied strictly, so every octet outside `pchar` is
/// refused and a percent sign must introduce two hexadecimal digits. Note
/// that this is a syntactic check alone. A dot-segment is well formed, so
/// this says nothing about where the reference lands within the origin and
/// is no defence against path traversal.
///
/// For example:
///
/// ```cpp
/// #include <sourcemeta/core/uri.h>
/// #include <cassert>
///
/// assert(sourcemeta::core::URI::is_absolute_path_reference("/schemas?page=2"));
/// assert(!sourcemeta::core::URI::is_absolute_path_reference("//example.com"));
/// assert(!sourcemeta::core::URI::is_absolute_path_reference("https://example.com"));
/// ```
[[nodiscard]] static auto
is_absolute_path_reference(std::string_view input) noexcept -> bool;

/// Check if the given string is a valid absolute IRI (has a scheme) per
/// RFC 3987 without constructing a full URI object. For example:
///
Expand Down
71 changes: 71 additions & 0 deletions src/core/uri/parse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,77 @@ auto URI::is_uri_reference(const std::string_view input) noexcept -> bool {
}
}

auto URI::is_absolute_path_reference(const std::string_view input) noexcept
-> bool {
// RFC 3986 Section 4.2: a relative reference beginning with a single slash.
// Two slashes would make it a network-path reference, naming an authority
// of its own, and the second slash is not a pchar so the path-absolute
// production refuses it on its own terms
if (input.empty() || input.front() != URI_SLASH) {
return false;
}

// RFC 3986 Section 3.4 and Section 3.5: query and fragment both admit the
// slash and the question mark on top of pchar, so the scan only has to widen
// once each delimiter is passed
bool within_path{true};
bool within_query{false};
for (std::string_view::size_type position{1}; position < input.size();
position += 1) {
const auto character{input[position]};
if (character == URI_SLASH) {
// RFC 3986 Section 3.3: segment-nz requires at least one pchar, so a
// second slash cannot open the path
if (within_path && position == 1) {
return false;
}

continue;
}

if (character == URI_QUESTION) {
if (within_path) {
within_path = false;
within_query = true;
continue;
}

// A further question mark is content within a query or a fragment
continue;
}

if (character == URI_HASH) {
// RFC 3986 Section 3.5: the number sign is outside the fragment
// character set, so only the first one delimits
if (!within_path && !within_query) {
return false;
}

within_path = false;
within_query = false;
continue;
}

// RFC 3986 Section 2.1: pct-encoded = "%" HEXDIG HEXDIG, which the shared
// character predicate admits the percent sign for without checking
if (character == URI_PERCENT) {
if (position + 2 >= input.size() || !is_hex_digit(input[position + 1]) ||
!is_hex_digit(input[position + 2])) {
return false;
}

position += 2;
continue;
}

if (!uri_is_pchar(character)) {
return false;
}
}

return true;
}

auto URI::is_iri(const std::string_view input) noexcept -> bool {
try {
std::optional<std::string> scheme, userinfo, host, path, query, fragment;
Expand Down
1 change: 1 addition & 0 deletions test/uri/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME uri
uri_port_test.cc
uri_scheme_test.cc
uri_is_scheme_test.cc
uri_is_absolute_path_reference_test.cc
uri_is_gen_delim_test.cc
uri_query_test.cc
uri_is_absolute_test.cc
Expand Down
200 changes: 200 additions & 0 deletions test/uri/uri_is_absolute_path_reference_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#include <sourcemeta/core/test.h>
#include <sourcemeta/core/uri.h>

#include <string> // std::string

// RFC 3986 §4.2: "A relative reference that begins with a single slash
// character is termed an absolute-path reference"
TEST(is_absolute_path_reference_root) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/"));
}

TEST(is_absolute_path_reference_single_segment) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/schemas"));
}

TEST(is_absolute_path_reference_several_segments) {
EXPECT_TRUE(
sourcemeta::core::URI::is_absolute_path_reference("/schemas/example"));
}

// RFC 3986 §3.3: segment = *pchar, so a trailing empty segment is well formed
TEST(is_absolute_path_reference_trailing_slash) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/schemas/"));
}

// RFC 3986 §4.2: a reference beginning with two slashes is a network-path
// reference, which resolves against another authority
TEST(is_absolute_path_reference_network_path) {
EXPECT_FALSE(
sourcemeta::core::URI::is_absolute_path_reference("//example.com/evil"));
}

TEST(is_absolute_path_reference_two_slashes_alone) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("//"));
}

TEST(is_absolute_path_reference_empty) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference(""));
}

// RFC 3986 §4.2: a reference not beginning with a slash is a relative-path
// reference
TEST(is_absolute_path_reference_relative_path) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("schemas"));
}

TEST(is_absolute_path_reference_absolute_uri) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference(
"https://example.com/evil"));
}

TEST(is_absolute_path_reference_scheme_relative_without_slash) {
EXPECT_FALSE(
sourcemeta::core::URI::is_absolute_path_reference("https:/evil"));
}

// RFC 3986 §3.3: the backslash is not a pchar, and a browser folds it to a
// slash, which would make this a network-path reference
TEST(is_absolute_path_reference_backslash_after_slash) {
EXPECT_FALSE(
sourcemeta::core::URI::is_absolute_path_reference("/\\example.com"));
}

TEST(is_absolute_path_reference_leading_backslashes) {
EXPECT_FALSE(
sourcemeta::core::URI::is_absolute_path_reference("\\\\example.com"));
}

TEST(is_absolute_path_reference_backslash_within_a_segment) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a\\b"));
}

// RFC 3986 §4.2: relative-ref = relative-part [ "?" query ] [ "#" fragment ]
TEST(is_absolute_path_reference_with_a_query) {
EXPECT_TRUE(
sourcemeta::core::URI::is_absolute_path_reference("/schemas?page=2"));
}

TEST(is_absolute_path_reference_with_a_fragment) {
EXPECT_TRUE(
sourcemeta::core::URI::is_absolute_path_reference("/schemas#section"));
}

TEST(is_absolute_path_reference_with_a_query_and_a_fragment) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference(
"/schemas?page=2#section"));
}

TEST(is_absolute_path_reference_with_an_empty_query) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/schemas?"));
}

TEST(is_absolute_path_reference_with_an_empty_fragment) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/schemas#"));
}

// RFC 3986 §3.4: query = *( pchar / "/" / "?" )
TEST(is_absolute_path_reference_query_admits_slashes_and_questions) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/a?b=/c?d=e"));
}

// RFC 3986 §3.5: fragment = *( pchar / "/" / "?" )
TEST(is_absolute_path_reference_fragment_admits_slashes_and_questions) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/a#b/c?d"));
}

// RFC 3986 §3.5: the number sign is not admitted within a fragment, so only
// the first one delimits
TEST(is_absolute_path_reference_second_number_sign) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a#b#c"));
}

// RFC 3986 §2.1: pct-encoded = "%" HEXDIG HEXDIG
TEST(is_absolute_path_reference_percent_encoded) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/a%20b"));
}

TEST(is_absolute_path_reference_percent_encoded_lowercase) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/a%2fb"));
}

TEST(is_absolute_path_reference_bare_percent) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a%"));
}

TEST(is_absolute_path_reference_truncated_percent_encoding) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a%2"));
}

TEST(is_absolute_path_reference_non_hexadecimal_percent_encoding) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a%zz"));
}

TEST(is_absolute_path_reference_percent_encoding_in_a_query) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/a?b=%20"));
}

TEST(is_absolute_path_reference_bare_percent_in_a_fragment) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a#%"));
}

// RFC 3986 §3.3: pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
TEST(is_absolute_path_reference_unreserved_characters) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/aZ9-._~"));
}

TEST(is_absolute_path_reference_sub_delimiters) {
EXPECT_TRUE(
sourcemeta::core::URI::is_absolute_path_reference("/a!$&'()*+,;="));
}

TEST(is_absolute_path_reference_colon_and_at_sign) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/a:b@c"));
}

TEST(is_absolute_path_reference_space) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a b"));
}

TEST(is_absolute_path_reference_control_character) {
EXPECT_FALSE(
sourcemeta::core::URI::is_absolute_path_reference(std::string{"/a\x01"
"b"}));
}

TEST(is_absolute_path_reference_carriage_return) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a\r\nb"));
}

TEST(is_absolute_path_reference_delete_character) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a\x7f"));
}

TEST(is_absolute_path_reference_non_ascii) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a\xc3\xa9"));
}

// Every one of these is outside pchar, so a faithful validator refuses them
// where a permissive one would let them reach a Location header unencoded
TEST(is_absolute_path_reference_characters_outside_pchar) {
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a<b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a>b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a\"b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a{b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a}b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a|b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a^b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a`b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a[b"));
EXPECT_FALSE(sourcemeta::core::URI::is_absolute_path_reference("/a]b"));
}

// The check is syntactic and says nothing about where a reference resolves
// within the origin, so a dot-segment is well formed and stays the caller's
// concern
TEST(is_absolute_path_reference_dot_segments) {
EXPECT_TRUE(sourcemeta::core::URI::is_absolute_path_reference("/.."));
EXPECT_TRUE(
sourcemeta::core::URI::is_absolute_path_reference("/../../etc/passwd"));
}
Loading