Skip to content

Commit

Permalink
fix: ensure crucial security measure is implemented per https://rxdb.…
Browse files Browse the repository at this point in the history
  • Loading branch information
rbeauchamp committed Sep 16, 2024
1 parent 5edef13 commit c08954e
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 14 deletions.
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
# RxDBDotNet

<p align="left">
<a href="https://www.nuget.org/packages/RxDBDotNet/" style="text-decoration:none;">
<img src="https://img.shields.io/nuget/v/RxDBDotNet.svg" alt="NuGet Version" style="margin-right: 10px;">
</a>
<a href="https://www.nuget.org/packages/RxDBDotNet/" style="text-decoration:none;">
<img src="https://img.shields.io/nuget/dt/RxDBDotNet.svg" alt="NuGet Downloads" style="margin-right: 10px;">
</a>
<a href="https://codecov.io/github/Ziptility/RxDBDotNet" style="text-decoration:none;">
<img src="https://codecov.io/github/Ziptility/RxDBDotNet/graph/badge.svg?token=VvuBJEsIHT" alt="codecov">
</a>
</p>
[![NuGet Version](https://img.shields.io/nuget/v/RxDBDotNet.svg)](https://www.nuget.org/packages/RxDBDotNet/)
[![NuGet Downloads](https://img.shields.io/nuget/dt/RxDBDotNet.svg)](https://www.nuget.org/packages/RxDBDotNet/)
[![codecov](https://codecov.io/github/Ziptility/RxDBDotNet/graph/badge.svg?token=VvuBJEsIHT)](https://codecov.io/github/Ziptility/RxDBDotNet)

RxDBDotNet is a powerful .NET library that implements the [RxDB replication protocol](https://rxdb.info/replication.html), enabling real-time data synchronization between RxDB clients and .NET servers using GraphQL and Hot Chocolate. It extends the standard RxDB replication protocol with .NET-specific enhancements.

Expand All @@ -37,6 +29,7 @@ Ready to dive in? [Get started](#getting-started) or [contribute](#contributing)
- [Sample Implementation](#sample-implementation)
- [RxDB Replication Protocol Details](#rxdb-replication-protocol-details)
- [Advanced Features](#advanced-features)
- [Security Considerations](#security-considerations)
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [License](#license)
Expand Down Expand Up @@ -642,6 +635,20 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
This feature allows for more robust and flexible authentication scenarios, particularly in environments where signing keys may change dynamically or where you're integrating with external OIDC providers like IdentityServer.
## Security Considerations
### Server-Side Timestamp Overwriting
RxDBDotNet implements a [crucial security measure](https://rxdb.info/replication.html#security) to prevent potential issues with untrusted client-side clocks. When the server receives a document creation or update request, it always overwrites the `UpdatedAt` timestamp with its own server-side timestamp. This approach ensures that:
1. The integrity of the document's timeline is maintained.
2. Potential time-based attacks or inconsistencies due to client clock discrepancies are mitigated.
3. The server maintains authoritative control over the timestamp for all document changes.
This security measure is implemented in the `MutationResolver<TDocument>` class, which handles document push operations. Developers using RxDBDotNet should be aware that any client-provided `UpdatedAt` value will be ignored and replaced with the server's timestamp.
Important: While the `IReplicatedDocument` interface defines `UpdatedAt` with both a getter and a setter, developers should not manually set this property in their application code. Always rely on the server to set the correct `UpdatedAt` value during replication operations. The setter is present solely to allow the server to overwrite the timestamp as a security measure.
## Contributing
We welcome contributions to RxDBDotNet! Here's how you can contribute:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace LiveDocs.GraphQLApi.Models.Replication;
public abstract record ReplicatedDocument : IReplicatedDocument
{
private readonly List<string>? _topics;
private readonly DateTimeOffset _updatedAt;
private DateTimeOffset _updatedAt;

/// <inheritdoc />
[Required]
Expand All @@ -27,7 +27,7 @@ public abstract record ReplicatedDocument : IReplicatedDocument
public required DateTimeOffset UpdatedAt
{
get => _updatedAt;
init =>
set =>
// Strip microseconds by setting the ticks to zero, keeping only up to milliseconds.
// Doing this because microseconds are not supported by Hot Chocolate's DateTime serializer.
// Now Equals() and GetHashCode() will work correctly.
Expand Down
3 changes: 2 additions & 1 deletion src/RxDBDotNet/Documents/IReplicatedDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ public interface IReplicatedDocument
/// <remarks>
/// This property is crucial for conflict resolution and determining the most recent version of a document.
/// It should be updated every time the document is modified.
/// The server will always overwrite this value with its own timestamp to ensure security and consistency.
/// </remarks>
DateTimeOffset UpdatedAt { get; }
DateTimeOffset UpdatedAt { get; set; }

/// <summary>
/// A value indicating whether the document has been marked as deleted.
Expand Down
11 changes: 11 additions & 0 deletions src/RxDBDotNet/Resolvers/MutationResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ private static async Task<List<TDocument>> ApplyChangesAsync(
await AuthorizeOperationAsync(authorizationHelper, currentUser, securityOptions, Operation.Create)
.ConfigureAwait(false);

// Set the server timestamp to ensure data integrity and security
// This overrides any client-provided timestamp, as client-side clocks cannot be trusted
// It's crucial for maintaining a reliable timeline of document changes and preventing potential exploits
create.UpdatedAt = DateTimeOffset.UtcNow;
await documentService.CreateDocumentAsync(create, cancellationToken)
.ConfigureAwait(false);
}
Expand Down Expand Up @@ -236,6 +240,13 @@ private static async Task HandleDocumentUpdateAsync(
SecurityOptions<TDocument>? securityOptions,
CancellationToken cancellationToken)
{
// Set the server timestamp for updates
// This is a critical security measure that ensures:
// 1. The integrity of the document's timeline is maintained
// 2. Potential time-based attacks or inconsistencies due to client clock discrepancies are mitigated
// 3. The server has the authoritative timestamp for all document changes
update.UpdatedAt = DateTimeOffset.UtcNow;

if (update.IsDeleted)
{
await AuthorizeOperationAsync(authorizationHelper, currentUser, securityOptions, Operation.Delete)
Expand Down
58 changes: 58 additions & 0 deletions tests/RxDBDotNet.Tests/PushDocumentsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,64 @@ public async Task DisposeAsync()
await TestContext.DisposeAsync();
}

[Fact]
public async Task PushDocuments_ShouldOverwriteClientProvidedTimestamp()
{
// Arrange
TestContext = new TestScenarioBuilder().Build();
var workspaceId = Provider.Sql.Create();
var clientProvidedTimestamp = DateTimeOffset.UtcNow.AddDays(-1); // Simulate an old timestamp

var newWorkspace = new WorkspaceInputGql
{
Id = workspaceId,
Name = Strings.CreateString(),
UpdatedAt = clientProvidedTimestamp, // Use the old timestamp
IsDeleted = false,
Topics = new List<string>
{
workspaceId.ToString(),
},
};

var workspaceInputPushRowGql = new WorkspaceInputPushRowGql
{
AssumedMasterState = null,
NewDocumentState = newWorkspace,
};

var pushWorkspaceInputGql = new PushWorkspaceInputGql
{
WorkspacePushRow = new List<WorkspaceInputPushRowGql?>
{
workspaceInputPushRowGql,
},
};

var createWorkspace =
new MutationQueryBuilderGql().WithPushWorkspace(new PushWorkspacePayloadQueryBuilderGql().WithAllFields(), pushWorkspaceInputGql);

// Act
var pushResponse = await TestContext.HttpClient.PostGqlMutationAsync(createWorkspace, TestContext.CancellationToken);

// Assert push response
pushResponse.Errors.Should().BeNullOrEmpty();
pushResponse.Data.PushWorkspace?.Errors.Should().BeNullOrEmpty();
pushResponse.Data.PushWorkspace?.Workspace.Should().BeNullOrEmpty();

// Verify the created workspace
var createdWorkspace = await TestContext.HttpClient.GetWorkspaceByIdAsync(workspaceId, TestContext.CancellationToken);

createdWorkspace.Should().NotBeNull();
createdWorkspace.Id.Should().Be(workspaceId);
createdWorkspace.Name.Should().Be(newWorkspace.Name.Value);
createdWorkspace.IsDeleted.Should().Be(newWorkspace.IsDeleted);

// Check if the timestamp was overwritten by the server
createdWorkspace.UpdatedAt.Should().BeAfter(clientProvidedTimestamp);
createdWorkspace.UpdatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(1));
}

[Fact]
public async Task PushDocuments_WithNullDocumentList_ShouldReturnEmptyResult()
{
Expand Down

0 comments on commit c08954e

Please sign in to comment.