Skip to content
Draft
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
14 changes: 13 additions & 1 deletion src/RestSharp/Authenticators/OAuth/OAuthTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,19 @@ static string ConstructRequestUrl(Uri url) {
var secure = url is { Scheme: "https", Port: 443 };
var port = basic || secure ? "" : $":{url.Port}";

return $"{url.Scheme}://{url.Host}{port}{url.AbsolutePath}";
// Decode the path to avoid double-encoding when the path contains already-encoded characters.
// For example, if a URL segment was added with AddUrlSegment("id", "value!"), it gets encoded
// to "value%21" in the URL. When we extract url.AbsolutePath, it contains "%21" (encoded).
// If we then call UrlEncodeRelaxed on it, Uri.EscapeDataString would encode the "%" to "%25",
// resulting in "%2521" (double-encoded). By decoding first, we ensure proper single encoding.
//
// Security note: This is safe because:
// - The url parameter is a validated Uri object constructed by RestSharp's BuildUri()
// - The decoded path is immediately re-encoded by UrlEncodeRelaxed before use
// - There is no direct user input involved in this internal OAuth signature calculation
var decodedPath = Uri.UnescapeDataString(url.AbsolutePath);

return $"{url.Scheme}://{url.Host}{port}{decodedPath}";
}

/// <summary>
Expand Down
71 changes: 71 additions & 0 deletions test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,75 @@ public void Generates_correct_signature_base() {
"POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521"
);
}

[Fact]
public void Handles_path_with_exclamation_mark() {
// Test that a path segment with ! is encoded correctly in the signature base
var client = new RestClient("https://api.example.com");
var request = new RestRequest("path/with!exclamation/resource", Method.Get);

const string method = "GET";
var url = client.BuildUri(request).ToString();
var parameters = new WebPairCollection();

_workflow.RequestUrl = url;
var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters);

var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters);

// The URL should be encoded with ! as %21 in the signature base
signatureBase.Should().Contain("path%2Fwith%21exclamation%2Fresource");
}

[Theory]
[InlineData("path/with!exclamation", "%21")]
[InlineData("path/with*asterisk", "%2A")]
[InlineData("path/with'apostrophe", "%27")]
[InlineData("path/with(paren", "%28")]
[InlineData("path/with)paren", "%29")]
public void Encodes_RFC3986_special_chars_in_path(string path, string encodedChar) {
// Test that RFC 3986 special characters are properly encoded in path segments
var client = new RestClient("https://api.example.com");
var request = new RestRequest(path, Method.Get);

const string method = "GET";
var url = client.BuildUri(request).ToString();
var parameters = new WebPairCollection();

_workflow.RequestUrl = url;
var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters);

var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters);

// The URL should contain the encoded character in the signature base
signatureBase.Should().Contain(encodedChar);
}

[Theory]
[InlineData("with!exclamation")]
[InlineData("with*asterisk")]
[InlineData("with'apostrophe")]
[InlineData("with(paren")]
[InlineData("with)paren")]
public void Handles_url_segment_with_RFC3986_special_chars(string segmentValue) {
// Test that URL segment parameters with RFC 3986 special characters don't get double-encoded
var client = new RestClient("https://api.example.com");
var request = new RestRequest("path/{segment}/resource", Method.Get);
request.AddUrlSegment("segment", segmentValue);

const string method = "GET";
var url = client.BuildUri(request).ToString();
var parameters = new WebPairCollection();

_workflow.RequestUrl = url;
var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters);

var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters);

// The signature base should NOT contain double-encoded characters like %2521 (which is %25 + 21)
signatureBase.Should().NotContain("%25");

// But it should contain properly encoded special chars
signatureBase.Should().MatchRegex("%2[0-9A-F]");
}
}