HIGH replay attackecho godynamodb

Replay Attack in Echo Go with Dynamodb

Replay Attack in Echo Go with Dynamodb — how this specific combination creates or exposes the vulnerability

A replay attack in the context of an Echo Go service that uses Amazon DynamoDB occurs when an attacker captures a valid request—such as an HTTP API call authenticated with temporary credentials or a signed payload—and re-sends it to the same endpoint to produce an unauthorized effect. Because Echo Go routes HTTP requests into business logic that reads from and writes to DynamoDB, the vulnerability arises when the application does not adequately bind requests to a specific execution context (e.g., a nonce, timestamp, or one-time token) before performing DynamoDB operations like GetItem, PutItem, or UpdateItem.

In this setup, the risk is amplified when requests contain an identifier (such as a user ID or order ID) that directly maps to a DynamoDB key (partition key or sort key). If the server processes the same captured request idempotently—such as transferring money, updating order status, or creating a resource—without verifying freshness, the backend will execute the operation again, leading to double-spend, duplicate records, or unauthorized state changes. Because DynamoDB is often the source of truth, a replayed request can overwrite or append data in a way that appears valid to the application but was not intended by the legitimate user.

Another vector specific to Echo Go with DynamoDB is when the service uses unauthenticated or weakly authenticated endpoints (for example, a public API Gateway route that eventually calls DynamoDB via the AWS SDK). An attacker can intercept a signed request made by a legitimate client, then replay it against the public endpoint. If the service does not validate one-time use or include per-request nonces, DynamoDB operations such as conditional writes (using ConditionExpression) may be bypassed if the attacker knows or omits the condition, or they may succeed because the condition still evaluates to true on replay.

In practice, this mirrors common web API weaknesses around missing replay protection and insufficient idempotency controls, and it becomes critical when DynamoDB is used to store sensitive financial or state-changing records. The attack does not require breaking encryption; it exploits the logical flow where identical inputs produce identical side effects in DynamoDB. Tools such as middleBrick can surface this class of risk by scanning the unauthenticated attack surface and flagging missing nonce or timestamp validation alongside insecure usage patterns in DynamoDB-driven endpoints.

Dynamodb-Specific Remediation in Echo Go — concrete code fixes

Remediation focuses on ensuring each request is unique and verifying intent before performing DynamoDB operations in Echo Go. Use a combination of client-supplied nonces or timestamps, server-side idempotency keys stored in DynamoDB, and conditional expressions that include freshness checks. The following examples assume an Echo Go route handling a payment request that writes to a DynamoDB table named Payments.

1. Enforce per-request nonces and idempotency keys

Require the client to send a nonce (e.g., a UUID) or a client-generated idempotency key with every request. Store the key in DynamoDB with the request outcome, and reject duplicates.

// Echo Go handler example
import (
    "context"
    "github.com/labstack/echo/v4"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
    "time"
)

type PaymentRequest struct {
    UserID    string `json:"userId"`
    Amount    int64  `json:"amount"`
    IdempotencyKey string `json:"idempotencyKey"`
}

func MakePayment(c echo.Context) error {
    req := new(PaymentRequest)
    if err := c.Bind(req).Error; err != nil {
        return echo.NewHTTPError(400, "invalid payload")
    }
    if req.IdempotencyKey == "" {
        return echo.NewHTTPError(400, "idempotencyKey is required")
    }

    client := dynamodb.NewFromConfig(cfg)
    ctx := c.Request().Context()

    // Check if idempotency key already exists
    out, err := client.GetItem(ctx, &dynamodb.GetItemInput{
        TableName: aws.String("IdempotencyKeys"),
        Key: map[string]types.AttributeValue{
            "IdempotencyKey": &types.AttributeValueMemberS{Value: req.IdempotencyKey},
        },
    })
    if err != nil {
        return echo.NewHTTPError(500, "failed to check idempotency")
    }
    if out.Item != nil {
        // Already processed; return the original response
        var resp PaymentResponse
        if err := dynamodbattribute.UnmarshalMap(out.Item, &resp); err != nil {
            return echo.NewHTTPError(500, "failed to decode cached response")
        }
        return c.JSON(200, resp)
    }

    // Perform payment logic and write to Payments table and IdempotencyKeys atomically if possible
    // For simplicity, we write idempotency record first with status PENDING, then proceed
    _, err = client.PutItem(ctx, &dynamodb.PutItemInput{
        TableName: aws.String("IdempotencyKeys"),
        Item: map[string]types.AttributeValue{
            "IdempotencyKey": &types.AttributeValueMemberS{Value: req.IdempotencyKey},
            "Status":         &types.AttributeValueMemberS{Value: "PENDING"},
            "Result":         &types.AttributeValueMemberS{Value: ""},
            "ExpiresAt":      &types.AttributeValueMemberN{Value: strconv.FormatInt(time.Now().Add(24*time.Hour).Unix(), 10)},
        },
    })
    if err != nil {
        return echo.NewHTTPError(500, "failed to create idempotency record")
    }

    // Execute payment write to Payments table
    _, err = client.PutItem(ctx, &dynamodb.PutItemInput{
        TableName: aws.String("Payments"),
        Item: map[string]types.AttributeValue{
            "PaymentID": &types.AttributeValueMemberS{Value: generateUUID()},
            "UserID":    &types.AttributeValueMemberS{Value: req.UserID},
            "Amount":    &types.AttributeValueMemberN{Value: strconv.FormatInt(req.Amount, 10)},
        },
    })
    if err != nil {
        // Optionally mark idempotency record as failed
        return echo.NewHTTPError(500, "payment failed")
    }

    // Update idempotency record to COMPLETED with result
    _, err = client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
        TableName: aws.String("IdempotencyKeys"),
        Key: map[string]types.AttributeValue{
            "IdempotencyKey": &types.AttributeValueMemberS{Value: req.IdempotencyKey},
        },
        UpdateExpression: aws.String("set #s = :c, #r = :r"),
        ExpressionAttributeNames: map[string]string{
            "#s": "Status",
            "#r": "Result",
        },
        ExpressionAttributeValues: map[string]types.AttributeValue{
            ":c": &types.AttributeValueMemberS{Value: "COMPLETED"},
            ":r": &types.AttributeValueMemberS{Value: "success"},
        },
    })
    if err != nil {
        // Log but do not fail the user request; idempotency is best-effort here
    }

    return c.JSON(200, map[string]string{"status": "ok"})
}

2. Use conditional writes with freshness checks

When updating an item, include an expected version or timestamp condition to ensure the write applies only if the item has not changed since the client last read it. This prevents replayed writes from silently overwriting newer data.

func UpdateOrderStatus(c echo.Context) error {
    orderID := c.Param("orderID")
    status := c.QueryParam("status")
    clientVersion := c.QueryParam("version") // e.g., timestamp or version number from client

    client := dynamodb.NewFromConfig(cfg)
    ctx := c.Request().Context()

    // Fetch current item to compare version
    out, err := client.GetItem(ctx, &dynamodb.GetItemInput{
        TableName: aws.String("Orders"),
        Key: map[string]types.AttributeValue{
            "OrderID": &types.AttributeValueMemberS{Value: orderID},
        },
    })
    if err != nil || out.Item == nil {
        return echo.NewHTTPError(404, "order not found")
    }
    currentVersion := string(out.Item["Version"].(*types.AttributeValueMemberS).Value)
    if currentVersion != clientVersion {
        return echo.NewHTTPError(409, "version mismatch — possible replay or concurrent update")
    }

    // Proceed with update using condition expression on version
    _, err = client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
        TableName: aws.String("Orders"),
        Key: map[string]types.AttributeValue{
            "OrderID": &types.AttributeValueMemberS{Value: orderID},
        },
        UpdateExpression: aws.String("set #s = :s, #v = :v"),
        ConditionExpression: aws.String("attribute_exists(OrderID) AND Version = :v"),
        ExpressionAttributeNames: map[string]string{
            "#s": "Status",
            "#v": "Version",
        },
        ExpressionAttributeValues: map[string]types.AttributeValue{
            ":s": &types.AttributeValueMemberS{Value: status},
            ":v": &types.AttributeValueMemberS{Value: strconv.FormatInt(time.Now().Unix(), 10)},
        },
    })
    if err != nil {
        return echo.NewHTTPError(409, "conditional update failed — likely a replay or race condition")
    }

    return c.JSON(200, map[string]string{"status": "updated"})
}

3. Validate timestamps and reject stale requests

Include a timestamp in the request and reject it if it falls outside an acceptable window. Combine this with DynamoDB conditional writes to enforce freshness.

func TransferFunds(c echo.Context) error {
    req := new(struct {
        From   string `json:"from"`
        To     string `json:"to"`
        Amount int64  `json:"amount"`
        Ts     int64  `json:"timestamp"` // Unix seconds
    })
    if err := c.Bind(req).Error; err != nil {
        return echo.NewHTTPError(400, "invalid payload")
    }

    window := int64(30) // 30 seconds
    now := time.Now().Unix()
    if now-req.Ts > window || req.Ts-now > window {
        return echo.NewHTTPError(400, "timestamp outside allowed window — possible replay")
    }

    client := dynamodb.NewFromConfig(cfg)
    ctx := c.Request().Context()

    // Use a conditional write on a nonce table to ensure one-time use
    _, err := client.PutItem(ctx, &dynamodb.PutItemInput{
        TableName: aws.String("Nonces"),
        Item: map[string]types.AttributeValue{
            "Nonce": &types.AttributeValueMemberS{Value: fmt.Sprintf("%d-%s", req.Ts, req.From)},
            "Used":  &types.AttributeValueMemberBOOL{Value: false},
        },
        ConditionExpression: aws.String("attribute_not_exists(Nonce)"),
    })
    if err != nil {
        return echo.NewHTTPError(409, "nonce already used — possible replay attack")
    }

    // Proceed with transfer
    _, err = client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
        TableName: aws.String("Accounts"),
        Key: map[string]types.AttributeValue{
            "AccountID": &types.AttributeValueMemberS{Value: req.From},
        },
        UpdateExpression:          aws.String("ADD Balance :debit"),
        ConditionExpression:       aws.String("Balance >= :debit"),
        ExpressionAttributeValues: map[string]types.AttributeValue{":debit": &types.AttributeValueMemberN{Value: strconv.FormatInt(-req.Amount, 10)}}},
    )
    if err != nil {
        return echo.NewHTTPError(409, "insufficient funds or concurrent modification")
    }

    return c.JSON(200, map[string]string{"status": "transferred"})
}

By binding each operation to a unique, validated identifier and using DynamoDB’s conditional writes, Echo Go services can effectively mitigate replay risks. middleBrick scans can help detect missing replay protection by correlating endpoint behaviors with DynamoDB interaction patterns, highlighting findings tied to authentication and idempotency weaknesses.

Frequently Asked Questions

How can I test if my Echo Go + DynamoDB API is vulnerable to replay attacks?
Send the same authenticated request twice using a tool like curl or Postman without nonces or freshness checks. Observe whether the second request produces the same side effect (e.g., duplicate payment). Use middleBrick to scan your API endpoints; it flags missing idempotency controls and missing timestamp/nonce validation.
Does enabling DynamoDB conditional writes fully prevent replay attacks?
Conditional writes reduce risk when they include freshness checks (e.g., version or timestamp attributes), but they are not sufficient alone. You must also enforce per-request nonces or idempotency keys and validate time windows; otherwise, replayed requests with valid conditions may still succeed.