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
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ public async Task PurgeBucketCache_ShouldRequestTransformationsOnly_GivenTheTran
this.SingleRequest().Query.Should().Contain(pair => pair.Key == "transformations" && pair.Value.Contains("true"));
}

[TestMethod]
public async Task PurgeBucketCache_ShouldEncodeDelimiters_GivenABucketIdWithUrlDelimiters()
{
this.Respond("/storage/v1/cdn/*", "DELETE", 200, "{\"message\":\"success\"}");
await this.client.PurgeBucketCache("my?bucket");
this.SingleRequest().AbsoluteUrl.Should().EndWith("/storage/v1/cdn/my%3Fbucket",
"a raw '?' in the id would start a query string and purge the wrong bucket");
}

private void Respond(string path, string method, int statusCode, string body) =>
this.server.Given(Request.Create().WithPath(path).UsingMethod(method))
.RespondWith(Response.Create().WithStatusCode(statusCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,15 @@ public async Task PurgeCache_ShouldDeleteTheCdnObjectPathWithNoBodyAndReturnTheM
}
}

[TestMethod]
public async Task PurgeCache_ShouldEncodeDelimiters_GivenAKeyWithUrlDelimiters()
{
this.Respond("/storage/v1/cdn/*", "DELETE", 200, "{\"message\":\"success\"}");
await this.client.From(Bucket).PurgeCache("folder/a?b#c.png");
this.SingleRequest().AbsoluteUrl.Should().EndWith($"/storage/v1/cdn/{Bucket}/folder/a%3Fb%23c.png",
"an unescaped '?' or '#' truncates the key into a query string or fragment");
}

[TestMethod]
public async Task Upload_ShouldSurfaceStorageException_GivenNonJsonError()
{
Expand Down
8 changes: 8 additions & 0 deletions packages/Storage/Storage/Helpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
Expand Down Expand Up @@ -168,6 +169,13 @@ HttpRequestMessage CreateRequest()
StorageInstrumentation.RecordRequest(method, builder.Uri, statusCode, errorType, startTimestamp);
}
}

/// <summary>
/// Percent-encodes each segment of a storage path so a <c>?</c> or <c>#</c> in a key can't start
/// a query string or fragment, while <c>/</c> stays literal as the separator.
/// </summary>
internal static string EncodePath(string path) =>
string.Join("/", path.Split('/').Select(Uri.EscapeDataString));
}

public class GenericResponse
Expand Down
2 changes: 1 addition & 1 deletion packages/Storage/Storage/StorageBucketApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public async Task<string> CreateBucket(string id, BucketUpsertOptions? options =
CancellationToken cancellationToken = default
)
{
var url = options.ToPurgeUrl($"{this.Url}/cdn/{id}");
var url = options.ToPurgeUrl($"{this.Url}/cdn/{Helpers.EncodePath(id)}");
return Helpers.MakeRequestAsync<GenericResponse>(this.requestClient, this.Options.Retry, HttpMethod.Delete, url, null, this.Headers, cancellationToken);
}
}
2 changes: 1 addition & 1 deletion packages/Storage/Storage/StorageFileApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ public async Task<UploadSignedUrl> CreateUploadSignedUrl(string supabasePath)
CancellationToken cancellationToken = default
)
{
var url = options.ToPurgeUrl($"{this.Url}/cdn/{this.GetFinalPath(path)}");
var url = options.ToPurgeUrl($"{this.Url}/cdn/{Helpers.EncodePath(this.GetFinalPath(path))}");
return Helpers.MakeRequestAsync<GenericResponse>(this.requestClient, this.Options.Retry, HttpMethod.Delete, url, null, this.Headers, cancellationToken);
}

Expand Down
Loading