From 1ed172c08db28a2dd55ff7c6955f7eb39ac363cc Mon Sep 17 00:00:00 2001 From: gimlichael Date: Mon, 10 Aug 2026 23:01:10 +0200 Subject: [PATCH 1/2] Fix CI pipeline and symlink resolution for 2.0.0 - Enable macOS tests on pull_request events in CI workflow - Fix ContentRootValidator to recursively resolve chained symbolic links - Add comprehensive test coverage for symlink chains and root directory cases - Update README to clarify multi-OS CI behavior - Update CHANGELOG.md with fixes section This patch improves cross-platform symbolic link traversal and ensures complete test validation across all platforms during pull requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci-pipeline.yml | 2 +- CHANGELOG.md | 6 ++++ README.md | 2 +- .../Hosting/ContentRootValidator.cs | 13 ++++++-- .../Hosting/ContentRootValidatorTest.cs | 31 +++++++++++++++++++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 9b88565..9fd3caf 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -60,7 +60,7 @@ jobs: run: | set -euo pipefail - if [[ "$EVENT_NAME" == "workflow_dispatch" && "$RUN_MAC_INPUT" == "true" ]]; then + if [[ "$EVENT_NAME" == "pull_request" ]] || [[ "$EVENT_NAME" == "workflow_dispatch" && "$RUN_MAC_INPUT" == "true" ]]; then echo "run-mac-tests=true" >> "$GITHUB_OUTPUT" else echo "run-mac-tests=false" >> "$GITHUB_OUTPUT" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb4650..161f723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,12 @@ This is a **major** release representing a deliberate modernization of the Stati - **`ServeUnknownFileTypes = true`.** Unknown file types are now rejected by default; add explicit MIME mappings through configuration to serve additional types, - Legacy `Startup` class and associated extension methods. Modern minimal hosting replaces them. +### Fixed + +- Symbolic link resolution in `ContentRootValidator` now recursively follows intermediate symlinks instead of stopping at the first target, preventing traversal bypasses when symlinks chain through multiple levels, +- CI workflow now runs macOS tests on pull request events (previously only on manual dispatch) for complete multi-OS coverage in validation gates, +- Test coverage expanded to include chained symbolic link scenarios and root directory edge cases. + ### Migration See the "Migration from 1.4.0 to 2.0.0" section of `README.md` for the full `1.x` → `2.0.0` configuration mapping and behavioural notes. diff --git a/README.md b/README.md index 3fe27fd..7da784f 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ COPY ./cdnroot /cdnroot ## CI and container promotion -Pull requests run the Debug/Release build and Linux/Windows test matrices, optionally including macOS. They also build the Dockerfile once on Linux/amd64, generate an SPDX JSON SBOM, save the image with `docker save`, and upload the tarball as an artifact. No registry credentials or push permissions are available to pull-request builds. +Pull requests run the Debug/Release build and Linux, Windows, and macOS test matrices. They also build the Dockerfile once on Linux/amd64, generate an SPDX JSON SBOM, save the image with `docker save`, and upload the tarball as an artifact. No registry credentials or push permissions are available to pull-request builds. Manually dispatched runs keep macOS optional through `run_mac_tests` because of its additional cost and runtime. The saved image receives two tags: diff --git a/src/Codebelt.Cdn.Origin/Hosting/ContentRootValidator.cs b/src/Codebelt.Cdn.Origin/Hosting/ContentRootValidator.cs index 4e10667..5f48fef 100644 --- a/src/Codebelt.Cdn.Origin/Hosting/ContentRootValidator.cs +++ b/src/Codebelt.Cdn.Origin/Hosting/ContentRootValidator.cs @@ -130,9 +130,16 @@ private static string ResolveFinalDirectoryPath(DirectoryInfo directory) string candidate = Path.Combine(ResolveFinalDirectoryPath(parent), directory.Name); - return Directory.Exists(candidate) - ? Directory.ResolveLinkTarget(candidate, returnFinalTarget: true)?.FullName ?? candidate - : candidate; + if (!Directory.Exists(candidate)) + { + return candidate; + } + + FileSystemInfo? linkTarget = Directory.ResolveLinkTarget(candidate, returnFinalTarget: true); + + return linkTarget is null + ? candidate + : ResolveFinalDirectoryPath(new DirectoryInfo(linkTarget.FullName)); } private static string EnsureTrailingSeparator(string path) diff --git a/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs b/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs index d033b20..06cd080 100644 --- a/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs +++ b/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs @@ -106,6 +106,14 @@ public void ExposesApplicationFiles_ShouldReturnTrue_WhenPathsAreEqual() Assert.True(ContentRootValidator.ExposesApplicationFiles(temp.Path, temp.Path)); } + [Fact] + public void ExposesApplicationFiles_ShouldReturnTrue_WhenPathsAreRootDirectories() + { + string root = Path.GetPathRoot(Path.GetTempPath())!; + + Assert.True(ContentRootValidator.ExposesApplicationFiles(root, root)); + } + [Fact] public void ExposesApplicationFiles_ShouldReturnFalse_WhenDirectoriesAreSeparate() { @@ -136,6 +144,29 @@ public void ExposesApplicationFiles_ShouldReturnTrue_WhenContentRootIsSymbolicLi } } + [Fact] + public void ExposesApplicationFiles_ShouldReturnTrue_WhenSymbolicLinkTargetTraversesSymbolicLink() + { + using var applicationParent = new TempDirectory(); + var applicationDirectory = Directory.CreateDirectory(Path.Combine(applicationParent.Path, "app")).FullName; + using var aliasHost = new TempDirectory(); + var applicationParentAlias = Path.Combine(aliasHost.Path, "alias"); + CreateDirectorySymbolicLinkOrSkip(applicationParentAlias, applicationParent.Path); + using var linkHost = new TempDirectory(); + var contentRoot = Path.Combine(linkHost.Path, "content"); + CreateDirectorySymbolicLinkOrSkip(contentRoot, Path.Combine(applicationParentAlias, "app")); + + try + { + Assert.True(ContentRootValidator.ExposesApplicationFiles(contentRoot, applicationDirectory)); + } + finally + { + Directory.Delete(contentRoot); + Directory.Delete(applicationParentAlias); + } + } + [Fact] public void Validate_ShouldFail_WhenContentRootIsSymbolicLinkToApplicationParent() { From 462e070cbf5802835bf83c4709f66fa9632794bd Mon Sep 17 00:00:00 2001 From: gimlichael Date: Mon, 10 Aug 2026 23:18:03 +0200 Subject: [PATCH 2/2] Update ContentRootValidator tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Hosting/ContentRootValidatorTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs b/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs index 06cd080..4e29ba1 100644 --- a/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs +++ b/test/Codebelt.Cdn.Origin.Tests/Hosting/ContentRootValidatorTest.cs @@ -135,8 +135,9 @@ public void ExposesApplicationFiles_ShouldReturnTrue_WhenContentRootIsSymbolicLi { Assert.True(ContentRootValidator.ExposesApplicationFiles(contentRoot, application.Path)); - var probe = ContentRootValidator.Probe(contentRoot, application.Path); - Assert.Equal(Path.GetFullPath(application.Path), probe.ResolvedPath); + var targetProbe = ContentRootValidator.Probe(application.Path, application.Path); + var contentRootProbe = ContentRootValidator.Probe(contentRoot, application.Path); + Assert.Equal(targetProbe.ResolvedPath, contentRootProbe.ResolvedPath); } finally {