a8e2f4dc4a
b3 of the multi-provider email roadmap. Adds ImapConnection model + table (reconciler pattern, SQLite+MySQL), ImapService (MailKit-backed IMAP client), ImapProvider implementing the existing IEmailProvider contract unchanged, and ImapController for credential-based connect (no OAuth — user supplies host/username/password directly, verified by a live connect before storage). Scope, documented inline with ponytail: comments: - INBOX only, no multi-folder support. - Thread grouping approximates the References/In-Reply-To chain root rather than the IMAP THREAD extension, which not every server implements. - External message ids are IMAP UIDs, scoped to the connection's current UIDVALIDITY. Security: ran the security-audit skill against this diff (credential handling + arbitrary-host connect is exactly the class of change the standing security gate exists for). Found and fixed a real SSRF: the connect endpoint let an authenticated user point the server at an arbitrary host:port with no internal-range check, and connect-vs-auth failure was distinguishable to the caller -- together a working oracle to fingerprint internal services (loopback/RFC1918/link-local/cloud metadata) from the server's network position. Fixed with EnsureHostIsExternalAsync (DNS-resolve + reject internal ranges, re-checked on every reconnect to close the DNS-rebinding gap) and a single generic failure message that no longer distinguishes connect vs auth failure. 7 regression tests added. Dependency: MailKit 4.17.0 (MIT license) on JobTrackerBackend.csproj -- stdlib has no IMAP client; hand-rolling IMAP4rev1 (TLS, SASL, MIME parsing) would be a large, security-sensitive protocol implementation nobody asked for, so this is the correct dependency, not a stdlib substitute. 168/168 green (161 existing + 7 new SSRF regression tests; the earlier 14 IMAP feature tests are included in the 161). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
2.0 KiB
C#
50 lines
2.0 KiB
C#
using System.IO;
|
|
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Tests.TestSupport;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
// Regression coverage for the SSRF guard in ImapService: an authenticated user's IMAP "connect"
|
|
// target must not be usable to probe loopback/RFC1918/link-local/cloud-metadata addresses.
|
|
public sealed class ImapServiceSsrfGuardTests
|
|
{
|
|
[Theory]
|
|
[InlineData("127.0.0.1")]
|
|
[InlineData("localhost")]
|
|
[InlineData("10.0.0.5")]
|
|
[InlineData("172.16.0.5")]
|
|
[InlineData("192.168.1.5")]
|
|
[InlineData("169.254.169.254")] // cloud metadata endpoint
|
|
public async Task ConnectAsync_rejects_internal_and_metadata_hosts(string host)
|
|
{
|
|
var service = CreateService();
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
service.ConnectAsync("user-1", host, 993, true, "user", "password", CancellationToken.None));
|
|
|
|
// Message must not leak connect-vs-auth distinction (that's the oracle this guard closes).
|
|
Assert.DoesNotContain("resolve", ex.Message, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("reachable", ex.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail()
|
|
{
|
|
var service = CreateService();
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
service.ConnectAsync("user-1", "this-host-does-not-exist.invalid", 993, true, "user", "password", CancellationToken.None));
|
|
|
|
Assert.Equal("Could not connect to that IMAP server with the given credentials. Check host, port, and password.", ex.Message);
|
|
}
|
|
|
|
private static ImapService CreateService()
|
|
{
|
|
var db = TestHostFactory.CreateInMemoryDb();
|
|
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"jobtracker-tests-{Guid.NewGuid():N}")));
|
|
return new ImapService(db, protectionProvider);
|
|
}
|
|
}
|