Fix path injection, sensitive attribute exposure, and error body truncation
Security fixes from audit: - Escape user-supplied strings (ext_relation_id, interface_name) with url.PathEscape before interpolating into API URL paths, preventing path traversal via crafted values like "../admin" or "foo/bar" - Mark auth token URL attributes as Sensitive in both virtfusion_user_auth_token and virtfusion_user_server_auth_token resources, since the URL embeds the signed token - Truncate raw API error response bodies to 500 bytes in error messages to prevent leaking sensitive data from verbose Laravel error responses Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,21 @@ type APIError struct {
|
|||||||
Errors map[string][]string
|
Errors map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxErrorBodyLen is the maximum number of bytes from the API response body
|
||||||
|
// to include in error messages, to avoid leaking sensitive data from verbose
|
||||||
|
// error responses.
|
||||||
|
const maxErrorBodyLen = 500
|
||||||
|
|
||||||
func (e *APIError) Error() string {
|
func (e *APIError) Error() string {
|
||||||
if len(e.Errors) > 0 {
|
if len(e.Errors) > 0 {
|
||||||
return fmt.Sprintf("VirtFusion API error %d (%s): %v", e.StatusCode, e.Status, e.Errors)
|
return fmt.Sprintf("VirtFusion API error %d (%s): %v", e.StatusCode, e.Status, e.Errors)
|
||||||
}
|
}
|
||||||
if e.Body != "" {
|
if e.Body != "" {
|
||||||
return fmt.Sprintf("VirtFusion API error %d (%s): %s", e.StatusCode, e.Status, e.Body)
|
body := e.Body
|
||||||
|
if len(body) > maxErrorBodyLen {
|
||||||
|
body = body[:maxErrorBodyLen] + "... (truncated)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("VirtFusion API error %d (%s): %s", e.StatusCode, e.Status, body)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("VirtFusion API error %d (%s)", e.StatusCode, e.Status)
|
return fmt.Sprintf("VirtFusion API error %d (%s)", e.StatusCode, e.Status)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"terraform-provider-virtfusion/internal/client"
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
|
rawResp, err := d.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", url.PathEscape(data.ExtRelationID.ValueString())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error Reading User", err.Error())
|
resp.Diagnostics.AddError("Error Reading User", err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"terraform-provider-virtfusion/internal/client"
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ func (r *ServerFirewallResource) Create(ctx context.Context, req resource.Create
|
|||||||
iface := data.InterfaceName.ValueString()
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
// Enable the firewall
|
// Enable the firewall
|
||||||
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/enable", serverID, iface), nil)
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/enable", serverID, url.PathEscape(iface)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error enabling server firewall", err.Error())
|
resp.Diagnostics.AddError("Error enabling server firewall", err.Error())
|
||||||
return
|
return
|
||||||
@@ -153,7 +154,7 @@ func (r *ServerFirewallResource) Create(ctx context.Context, req resource.Create
|
|||||||
|
|
||||||
if len(rules) > 0 {
|
if len(rules) > 0 {
|
||||||
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
|
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
|
||||||
_, err = r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
|
_, err = r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, url.PathEscape(iface)), rulesReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error setting firewall rules", err.Error())
|
resp.Diagnostics.AddError("Error setting firewall rules", err.Error())
|
||||||
return
|
return
|
||||||
@@ -175,7 +176,7 @@ func (r *ServerFirewallResource) Read(ctx context.Context, req resource.ReadRequ
|
|||||||
serverID := data.ServerID.ValueInt64()
|
serverID := data.ServerID.ValueInt64()
|
||||||
iface := data.InterfaceName.ValueString()
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
result, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", serverID, iface))
|
result, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", serverID, url.PathEscape(iface)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var apiErr *client.APIError
|
var apiErr *client.APIError
|
||||||
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
@@ -247,7 +248,7 @@ func (r *ServerFirewallResource) Update(ctx context.Context, req resource.Update
|
|||||||
}
|
}
|
||||||
|
|
||||||
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
|
rulesReq := client.FirewallSetRulesRequest{Rules: rules}
|
||||||
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, url.PathEscape(iface)), rulesReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error updating firewall rules", err.Error())
|
resp.Diagnostics.AddError("Error updating firewall rules", err.Error())
|
||||||
return
|
return
|
||||||
@@ -268,7 +269,7 @@ func (r *ServerFirewallResource) Delete(ctx context.Context, req resource.Delete
|
|||||||
serverID := data.ServerID.ValueInt64()
|
serverID := data.ServerID.ValueInt64()
|
||||||
iface := data.InterfaceName.ValueString()
|
iface := data.InterfaceName.ValueString()
|
||||||
|
|
||||||
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/disable", serverID, iface), nil)
|
_, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/disable", serverID, url.PathEscape(iface)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var apiErr *client.APIError
|
var apiErr *client.APIError
|
||||||
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"terraform-provider-virtfusion/internal/client"
|
"terraform-provider-virtfusion/internal/client"
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := r.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
|
result, err := r.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", url.PathEscape(data.ExtRelationID.ValueString())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var apiErr *client.APIError
|
var apiErr *client.APIError
|
||||||
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
@@ -175,7 +176,7 @@ func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, r
|
|||||||
Email: data.Email.ValueString(),
|
Email: data.Email.ValueString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := r.client.Put(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()), body)
|
result, err := r.client.Put(ctx, fmt.Sprintf("/users/%s/byExtRelation", url.PathEscape(data.ExtRelationID.ValueString())), body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error updating user", err.Error())
|
resp.Diagnostics.AddError("Error updating user", err.Error())
|
||||||
return
|
return
|
||||||
@@ -203,7 +204,7 @@ func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := r.client.Delete(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
|
_, err := r.client.Delete(ctx, fmt.Sprintf("/users/%s/byExtRelation", url.PathEscape(data.ExtRelationID.ValueString())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var apiErr *client.APIError
|
var apiErr *client.APIError
|
||||||
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
if errors.As(err, &apiErr) && apiErr.IsNotFound() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"terraform-provider-virtfusion/internal/client"
|
"terraform-provider-virtfusion/internal/client"
|
||||||
@@ -74,6 +75,7 @@ func (r *UserAuthTokenResource) Schema(_ context.Context, _ resource.SchemaReque
|
|||||||
"url": schema.StringAttribute{
|
"url": schema.StringAttribute{
|
||||||
MarkdownDescription: "The authentication URL for the generated token.",
|
MarkdownDescription: "The authentication URL for the generated token.",
|
||||||
Computed: true,
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
PlanModifiers: []planmodifier.String{
|
PlanModifiers: []planmodifier.String{
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
},
|
},
|
||||||
@@ -114,7 +116,7 @@ func (r *UserAuthTokenResource) Create(ctx context.Context, req resource.CreateR
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiPath := fmt.Sprintf("/users/%s/authenticationTokens", data.ExtRelationID.ValueString())
|
apiPath := fmt.Sprintf("/users/%s/authenticationTokens", url.PathEscape(data.ExtRelationID.ValueString()))
|
||||||
rawResp, err := r.client.Post(ctx, apiPath, nil)
|
rawResp, err := r.client.Post(ctx, apiPath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError(
|
resp.Diagnostics.AddError(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"terraform-provider-virtfusion/internal/client"
|
"terraform-provider-virtfusion/internal/client"
|
||||||
@@ -96,7 +97,7 @@ func (r *UserPasswordResetResource) Create(ctx context.Context, req resource.Cre
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiPath := fmt.Sprintf("/users/%s/byExtRelation/resetPassword", data.ExtRelationID.ValueString())
|
apiPath := fmt.Sprintf("/users/%s/byExtRelation/resetPassword", url.PathEscape(data.ExtRelationID.ValueString()))
|
||||||
_, err := r.client.Post(ctx, apiPath, nil)
|
_, err := r.client.Post(ctx, apiPath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError(
|
resp.Diagnostics.AddError(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"terraform-provider-virtfusion/internal/client"
|
"terraform-provider-virtfusion/internal/client"
|
||||||
@@ -80,6 +81,7 @@ func (r *UserServerAuthTokenResource) Schema(_ context.Context, _ resource.Schem
|
|||||||
"url": schema.StringAttribute{
|
"url": schema.StringAttribute{
|
||||||
MarkdownDescription: "The authentication URL for the generated server token.",
|
MarkdownDescription: "The authentication URL for the generated server token.",
|
||||||
Computed: true,
|
Computed: true,
|
||||||
|
Sensitive: true,
|
||||||
PlanModifiers: []planmodifier.String{
|
PlanModifiers: []planmodifier.String{
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
},
|
},
|
||||||
@@ -120,7 +122,7 @@ func (r *UserServerAuthTokenResource) Create(ctx context.Context, req resource.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiPath := fmt.Sprintf("/users/%s/serverAuthenticationTokens/%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64())
|
apiPath := fmt.Sprintf("/users/%s/serverAuthenticationTokens/%d", url.PathEscape(data.ExtRelationID.ValueString()), data.ServerID.ValueInt64())
|
||||||
rawResp, err := r.client.Post(ctx, apiPath, nil)
|
rawResp, err := r.client.Post(ctx, apiPath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError(
|
resp.Diagnostics.AddError(
|
||||||
|
|||||||
Reference in New Issue
Block a user