|
3 | 3 | using System; |
4 | 4 | using System.IO; |
5 | 5 | using System.Runtime.InteropServices; |
| 6 | +using System.Runtime.Versioning; |
6 | 7 | using System.Text; |
7 | 8 | using System.Threading; |
8 | 9 | using System.Threading.Tasks; |
| 10 | +using Microsoft.SemanticKernel; |
9 | 11 | using Microsoft.SemanticKernel.Plugins.MsGraph; |
10 | 12 | using Moq; |
11 | 13 | using Xunit; |
@@ -656,6 +658,94 @@ public async Task UploadDoesNotFollowLinksOutsideAllowedDirectoryAsync() |
656 | 658 | connector.VerifyNoOtherCalls(); |
657 | 659 | } |
658 | 660 |
|
| 661 | + [SymbolicLinkTheory] |
| 662 | + [InlineData(false)] |
| 663 | + [InlineData(true)] |
| 664 | + public async Task UploadPathResolutionFailuresReturnTheUsualDenialAsync(bool emptyAllowedDirectories) |
| 665 | + { |
| 666 | + var allowed = Path.Combine(this._tempDirectory, "allowed"); |
| 667 | + Directory.CreateDirectory(allowed); |
| 668 | + var loop = Path.Combine(this._tempDirectory, "loop"); |
| 669 | + Directory.CreateSymbolicLink(loop, loop); |
| 670 | + Assert.Throws<InvalidOperationException>(() => PathUtilities.GetSafeFullPath(loop)); |
| 671 | + var connector = new Mock<ICloudDriveConnector>(MockBehavior.Strict); |
| 672 | + var plugin = new CloudDrivePlugin(connector.Object) |
| 673 | + { |
| 674 | + AllowedUploadDirectories = emptyAllowedDirectories ? [] : [allowed], |
| 675 | + AllowedUploadDestinationPaths = ["/"] |
| 676 | + }; |
| 677 | + var expected = await Assert.ThrowsAsync<InvalidOperationException>( |
| 678 | + () => plugin.UploadFileAsync(Path.Combine(this._tempDirectory, "missing.txt"), "/file.txt")); |
| 679 | + |
| 680 | + var actual = await Assert.ThrowsAsync<InvalidOperationException>( |
| 681 | + () => plugin.UploadFileAsync(Path.Combine(loop, "file.txt"), "/file.txt")); |
| 682 | + |
| 683 | + Assert.Equal(expected.Message, actual.Message); |
| 684 | + Assert.Null(actual.InnerException); |
| 685 | + connector.VerifyNoOtherCalls(); |
| 686 | + } |
| 687 | + |
| 688 | + [SymbolicLinkFact] |
| 689 | + public async Task UploadPathResolutionFailuresDoNotHideConfigurationErrorsAsync() |
| 690 | + { |
| 691 | + var loop = Path.Combine(this._tempDirectory, "loop"); |
| 692 | + Directory.CreateSymbolicLink(loop, loop); |
| 693 | + var expected = Assert.Throws<InvalidOperationException>(() => PathUtilities.GetSafeFullPath(loop)); |
| 694 | + var connector = new Mock<ICloudDriveConnector>(MockBehavior.Strict); |
| 695 | + var plugin = new CloudDrivePlugin(connector.Object) |
| 696 | + { |
| 697 | + AllowedUploadDirectories = [loop], |
| 698 | + AllowedUploadDestinationPaths = ["/"] |
| 699 | + }; |
| 700 | + |
| 701 | + var actual = await Assert.ThrowsAsync<InvalidOperationException>( |
| 702 | + () => plugin.UploadFileAsync(Path.Combine(this._tempDirectory, "file.txt"), "/file.txt")); |
| 703 | + |
| 704 | + Assert.Equal(expected.Message, actual.Message); |
| 705 | + connector.VerifyNoOtherCalls(); |
| 706 | + } |
| 707 | + |
| 708 | + [LinuxNonRootFact] |
| 709 | + [SupportedOSPlatform("linux")] |
| 710 | + public async Task InaccessibleUploadPathsDoNotRevealDetailsOrHideConfigurationErrorsAsync() |
| 711 | + { |
| 712 | + var allowed = Path.Combine(this._tempDirectory, "allowed"); |
| 713 | + var restricted = Path.Combine(this._tempDirectory, "restricted"); |
| 714 | + var nested = Path.Combine(restricted, "nested"); |
| 715 | + Directory.CreateDirectory(allowed); |
| 716 | + Directory.CreateDirectory(nested); |
| 717 | + var inaccessible = Path.Combine(nested, "file.txt"); |
| 718 | + await File.WriteAllTextAsync(inaccessible, "outside"); |
| 719 | + var connector = new Mock<ICloudDriveConnector>(MockBehavior.Strict); |
| 720 | + var plugin = new CloudDrivePlugin(connector.Object) |
| 721 | + { |
| 722 | + AllowedUploadDirectories = [allowed], |
| 723 | + AllowedUploadDestinationPaths = ["/"] |
| 724 | + }; |
| 725 | + var expected = await Assert.ThrowsAsync<InvalidOperationException>( |
| 726 | + () => plugin.UploadFileAsync(Path.Combine(this._tempDirectory, "missing.txt"), "/file.txt")); |
| 727 | + var originalMode = File.GetUnixFileMode(restricted); |
| 728 | + try |
| 729 | + { |
| 730 | + File.SetUnixFileMode(restricted, UnixFileMode.None); |
| 731 | + Assert.Throws<UnauthorizedAccessException>(() => PathUtilities.GetSafeFullPath(inaccessible)); |
| 732 | + |
| 733 | + var actual = await Assert.ThrowsAsync<InvalidOperationException>( |
| 734 | + () => plugin.UploadFileAsync(inaccessible, "/file.txt")); |
| 735 | + Assert.Equal(expected.Message, actual.Message); |
| 736 | + Assert.Null(actual.InnerException); |
| 737 | + |
| 738 | + plugin.AllowedUploadDirectories = [nested]; |
| 739 | + await Assert.ThrowsAsync<UnauthorizedAccessException>( |
| 740 | + () => plugin.UploadFileAsync(Path.Combine(allowed, "file.txt"), "/file.txt")); |
| 741 | + connector.VerifyNoOtherCalls(); |
| 742 | + } |
| 743 | + finally |
| 744 | + { |
| 745 | + File.SetUnixFileMode(restricted, originalMode); |
| 746 | + } |
| 747 | + } |
| 748 | + |
659 | 749 | public void Dispose() |
660 | 750 | { |
661 | 751 | Directory.Delete(this._tempDirectory, recursive: true); |
|
0 commit comments