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
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.Anthropic/Prompty.Anthropic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>2.0.0-beta.3</Version>
<Version>2.0.0-beta.4</Version>
<PackageId>Prompty.Anthropic</PackageId>
<Authors>Microsoft</Authors>
<Description>Anthropic provider for Prompty — executor and processor for Claude models via the Anthropic Messages API.</Description>
Expand Down
46 changes: 46 additions & 0 deletions runtime/csharp/Prompty.Core.Tests/LoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,52 @@ public void Load_FileRef_SymlinkEscape_Throws()
}
}

[Fact]
public void Load_FileRef_IntermediateDirectorySymlinkEscape_Throws()
{
var root = Directory.CreateTempSubdirectory("prompty-loader-");
try
{
var promptDir = Directory.CreateDirectory(Path.Combine(root.FullName, "prompts"));
var externalDir = Directory.CreateDirectory(Path.Combine(root.FullName, "external"));
File.WriteAllText(Path.Combine(externalDir.FullName, "secret.txt"), "secret");
Directory.CreateSymbolicLink(Path.Combine(promptDir.FullName, "assets"), externalDir.FullName);

var prompt = Path.Combine(promptDir.FullName, "bad.prompty");
File.WriteAllText(prompt, "---\nname: bad\ndescription: \"${file:assets/secret.txt}\"\n---\nHello\n");

var ex = Assert.Throws<InvalidOperationException>(() => PromptyLoader.Load(prompt));
Assert.Contains("outside allowed roots", ex.Message);
}
finally
{
root.Delete(recursive: true);
}
}

[Fact]
public async Task LoadAsync_FileRef_IntermediateDirectorySymlinkEscape_Throws()
{
var root = Directory.CreateTempSubdirectory("prompty-loader-");
try
{
var promptDir = Directory.CreateDirectory(Path.Combine(root.FullName, "prompts"));
var externalDir = Directory.CreateDirectory(Path.Combine(root.FullName, "external"));
File.WriteAllText(Path.Combine(externalDir.FullName, "secret.txt"), "secret");
Directory.CreateSymbolicLink(Path.Combine(promptDir.FullName, "assets"), externalDir.FullName);

var prompt = Path.Combine(promptDir.FullName, "bad.prompty");
File.WriteAllText(prompt, "---\nname: bad\ndescription: \"${file:assets/secret.txt}\"\n---\nHello\n");

var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => PromptyLoader.LoadAsync(prompt));
Assert.Contains("outside allowed roots", ex.Message);
}
finally
{
root.Delete(recursive: true);
}
}

// --- Tools ---

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.Core/Prompty.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>2.0.0-beta.3</Version>
<Version>2.0.0-beta.4</Version>
<PackageId>Prompty.Core</PackageId>
<Authors>Microsoft</Authors>
<Description>Prompty is an asset class and format for LLM prompts. Core library with loader, pipeline, renderers, parsers, and tracing.</Description>
Expand Down
42 changes: 33 additions & 9 deletions runtime/csharp/Prompty.Core/ReferenceResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,18 +171,42 @@ private static bool IsWithinRoot(string path, string root)
private static string GetCanonicalPath(string path)
{
var fullPath = Path.GetFullPath(path);
if (File.Exists(fullPath))
var root = Path.GetPathRoot(fullPath);
if (string.IsNullOrEmpty(root))
{
var file = new FileInfo(fullPath);
var target = file.ResolveLinkTarget(returnFinalTarget: true);
return Path.GetFullPath(target?.FullName ?? file.FullName);
return fullPath;
}
if (Directory.Exists(fullPath))

var canonicalPath = root;
var components = fullPath[root.Length..].Split(
[Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar],
StringSplitOptions.RemoveEmptyEntries);

foreach (var component in components)
{
canonicalPath = Path.Combine(canonicalPath, component);
var target = ResolveLinkTarget(canonicalPath);
if (target is not null)
{
canonicalPath = Path.GetFullPath(target.FullName);
}
}

return canonicalPath;
}

private static FileSystemInfo? ResolveLinkTarget(string path)
{
if (Directory.Exists(path))
{
var directory = new DirectoryInfo(fullPath);
var target = directory.ResolveLinkTarget(returnFinalTarget: true);
return Path.GetFullPath(target?.FullName ?? directory.FullName);
return new DirectoryInfo(path).ResolveLinkTarget(returnFinalTarget: true);
}
return fullPath;

if (File.Exists(path))
{
return new FileInfo(path).ResolveLinkTarget(returnFinalTarget: true);
}

return null;
}
}
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.Foundry/Prompty.Foundry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<NoWarn>OPENAI001;CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>2.0.0-beta.3</Version>
<Version>2.0.0-beta.4</Version>
<PackageId>Prompty.Foundry</PackageId>
<Authors>Microsoft</Authors>
<Description>Microsoft Foundry (Azure OpenAI) provider for Prompty — executor and processor for Azure-hosted models.</Description>
Expand Down
2 changes: 1 addition & 1 deletion runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<NoWarn>OPENAI001;CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>2.0.0-beta.3</Version>
<Version>2.0.0-beta.4</Version>
<PackageId>Prompty.OpenAI</PackageId>
<Authors>Microsoft</Authors>
<Description>OpenAI provider for Prompty — executor and processor for OpenAI chat, embedding, image, and agent APIs.</Description>
Expand Down
Loading