Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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")
}
Comment thread
wbreza marked this conversation as resolved.

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)
}
Comment thread
jongio marked this conversation as resolved.
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