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
6 changes: 6 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ overrides:
- filename: pkg/project/service_target_dotnet_containerapp.go
words:
- IMAGENAME
- filename: internal/vsrpc/server.go
words:
- CSWSH
- filename: pkg/extensions/manager.go
words:
- myext
- filename: extensions/microsoft.azd.extensions/internal/resources/languages/**/.gitignore
words:
- rsuser
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/cmd/vs_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ func (s *vsServerAction) Run(ctx context.Context) (*actions.ActionResult, error)
}

listener = tls.NewListener(listener, config)
res.CertificateBytes = new(base64.StdEncoding.EncodeToString(derBytes))
certBytesStr := base64.StdEncoding.EncodeToString(derBytes)
res.CertificateBytes = &certBytesStr
}

resString, err := json.Marshal(res)
Expand Down
5 changes: 4 additions & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,10 @@ func (a *InitAction) parseAndSetProjectResourceId(ctx context.Context) error {
}

// Create FoundryProjectsClient and get connections
foundryClient := azure.NewFoundryProjectsClient(foundryProject.AiAccountName, foundryProject.AiProjectName, a.credential)
foundryClient, err := azure.NewFoundryProjectsClient(foundryProject.AiAccountName, foundryProject.AiProjectName, a.credential)
if err != nil {
return fmt.Errorf("creating Foundry client: %w", err)
}
connections, err := foundryClient.GetAllConnections(ctx)
if err != nil {
fmt.Printf("Could not get Microsoft Foundry project connections to initialize AZURE_CONTAINER_REGISTRY_ENDPOINT: %v. Please set this environment variable manually.\n", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1351,7 +1351,10 @@ func (a *InitFromCodeAction) processExistingFoundryProject(ctx context.Context,
}

// Create FoundryProjectsClient and get connections
foundryClient := azure.NewFoundryProjectsClient(foundryProject.AccountName, foundryProject.ProjectName, a.credential)
foundryClient, err := azure.NewFoundryProjectsClient(foundryProject.AccountName, foundryProject.ProjectName, a.credential)
if err != nil {
return fmt.Errorf("creating Foundry client: %w", err)
}
connections, err := foundryClient.GetAllConnections(ctx)
if err != nil {
fmt.Printf("Could not get Microsoft Foundry project connections to initialize AZURE_CONTAINER_REGISTRY_ENDPOINT: %v. Please set this environment variable manually.\n", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
Expand All @@ -19,13 +21,25 @@ import (

// FoundryProjectsClient provides methods to interact with Microsoft Foundry projects
type FoundryProjectsClient struct {
baseEndpoint string
apiVersion string
pipeline runtime.Pipeline
baseEndpoint string
baseOriginURL *url.URL // cached parsed base URL for SSRF origin checks
apiVersion string
pipeline runtime.Pipeline
}

// NewFoundryProjectsClient creates a new instance of FoundryProjectsClient
func NewFoundryProjectsClient(accountName string, projectName string, cred azcore.TokenCredential) *FoundryProjectsClient {
func NewFoundryProjectsClient(
accountName string,
projectName string,
cred azcore.TokenCredential,
) (*FoundryProjectsClient, error) {
if strings.TrimSpace(accountName) == "" {
return nil, fmt.Errorf("accountName must not be empty")
}
if strings.TrimSpace(projectName) == "" {
return nil, fmt.Errorf("projectName must not be empty")
}

baseEndpoint := fmt.Sprintf("https://%s.services.ai.azure.com/api/projects/%s", accountName, projectName)

userAgent := fmt.Sprintf("azd-ext-azure-ai-agents/%s", version.Version)
Expand All @@ -48,11 +62,17 @@ func NewFoundryProjectsClient(accountName string, projectName string, cred azcor
clientOptions,
)

return &FoundryProjectsClient{
baseEndpoint: baseEndpoint,
apiVersion: "2025-11-15-preview",
pipeline: pipeline,
parsedBase, err := url.Parse(baseEndpoint)
if err != nil {
return nil, fmt.Errorf("invalid base endpoint URL: %w", err)
}

return &FoundryProjectsClient{
baseEndpoint: baseEndpoint,
baseOriginURL: parsedBase,
apiVersion: "2025-11-15-preview",
pipeline: pipeline,
}, nil
}

// Connection-related types
Expand Down Expand Up @@ -144,7 +164,8 @@ func (c *FoundryProjectsClient) GetPagedConnections(ctx context.Context) (*Paged
// GetConnectionWithCredentials retrieves a specific connection with its credentials
func (c *FoundryProjectsClient) GetConnectionWithCredentials(ctx context.Context, name string) (*Connection, error) {
targetEndpoint := fmt.Sprintf(
"%s/connections/%s/getConnectionWithCredentials?api-version=%s", c.baseEndpoint, name, c.apiVersion)
"%s/connections/%s/getConnectionWithCredentials?api-version=%s",
c.baseEndpoint, url.PathEscape(name), c.apiVersion)

req, err := runtime.NewRequest(ctx, http.MethodPost, targetEndpoint)
if err != nil {
Expand Down Expand Up @@ -191,6 +212,9 @@ func (c *FoundryProjectsClient) GetAllConnections(ctx context.Context) ([]Connec

// Continue fetching pages while there's a next link
for nextLink != nil && *nextLink != "" {
if err := c.validateNextLinkOrigin(*nextLink); err != nil {
return nil, fmt.Errorf("refusing to follow pagination link: %w", err)
}
pagedConnections, err := c.getNextPage(ctx, *nextLink)
if err != nil {
return nil, err
Expand All @@ -204,6 +228,36 @@ func (c *FoundryProjectsClient) GetAllConnections(ctx context.Context) ([]Connec
return allConnections, nil
}

// validateNextLinkOrigin ensures that a pagination nextLink URL points to the same
// origin (scheme + host) as the client's base endpoint. This prevents SSRF attacks
// where a malicious API response redirects pagination to an attacker-controlled server.
func (c *FoundryProjectsClient) validateNextLinkOrigin(nextLink string) error {
if c.baseOriginURL == nil {
return fmt.Errorf("base endpoint URL not initialized")
}

linkURL, err := url.Parse(nextLink)
if err != nil {
return fmt.Errorf("invalid nextLink URL: %w", err)
}

// Reject scheme-relative URLs (e.g., "//evil.com/path") and URLs without an explicit scheme.
// These could bypass origin checks or behave unpredictably.
if linkURL.Scheme == "" {
return fmt.Errorf("nextLink must have an explicit scheme, got %q", nextLink)
}

if !strings.EqualFold(linkURL.Scheme, c.baseOriginURL.Scheme) ||
!strings.EqualFold(linkURL.Host, c.baseOriginURL.Host) {
return fmt.Errorf(
"nextLink origin mismatch: expected %s://%s, got %s://%s",
c.baseOriginURL.Scheme, c.baseOriginURL.Host, linkURL.Scheme, linkURL.Host,
)
}

return nil
}

// getNextPage fetches a single page of connections from the given URL
func (c *FoundryProjectsClient) getNextPage(ctx context.Context, url string) (*PagedConnection, error) {
req, err := runtime.NewRequest(ctx, http.MethodGet, url)
Expand Down
Loading
Loading