Pii Leakage in Echo Go with Dynamodb
Pii Leakage in Echo Go with Dynamodb — how this specific combination creates or exposes the vulnerability
When building HTTP APIs in Go using the Echo framework and persisting data in DynamoDB, PII leakage commonly arises from a mismatch between application intent and data access patterns. Echo routes often deserialize JSON requests into Go structs that include fields for sensitive attributes such as email, phone, or national ID. If these structs are used directly as DynamoDB attribute values or keys without selective projection, sensitive fields can be written in full to the table. DynamoDB’s eventual-consistency model and flexible schema amplify this risk: conditional writes or batch operations may inadvertently expose PII across indexes or through unexpected item merges.
DynamoDB-specific issues surface when queries or scans retrieve more attributes than necessary. For example, using Scan without a filter or projection expression can return every attribute, including PII, to the application. In an Echo handler, if the response serializer does not explicitly omit sensitive fields, those fields can be included in API responses. Another vector is the use of DynamoDB Streams with Lambda integrations; if the consumer logs or mishandles stream records, PII can be exposed in logs or downstream systems. Weak access patterns in DynamoDB—such as relying on a Global Secondary Index (GSI) that includes PII in its key schema—can also surface sensitive data through query results that are cached or retained longer than needed.
Consider an Echo route that updates a user profile and writes to DynamoDB:
// Echo handler that writes request body directly to DynamoDB (risky)
c.Bind(&user)
av, err := attributevalue.MarshalMap(user)
if err != nil {
c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
input := &dynamodb.PutItemInput{
TableName: aws.String("users"),
Item: av,
}
_, err = svc.PutItem(context.TODO(), input)
if err != nil {
c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
If the User struct contains fields like Email or SSN, and the application does not strip or encrypt them, the entire item is stored with PII. Later, a query or scan that returns ALL_ATTRIBUTES can expose these fields to any caller with read access, intentionally or unintentionally. Echo’s flexible parameter binding can also cause PII to be reflected in logs or error messages when binding fails, especially if request payloads include sensitive data and the framework’s debug mode is enabled.
SSR and misconfigured indexes compound these issues. A GSI that includes PII in its partition or sort key can propagate sensitive data into index queries. Without strict attribute selection on reads (e.g., using ProjectionExpression), consumers may retrieve full items or sensitive GSI keys. In multi-tenant scenarios, incorrect key design can allow one tenant’s PII to be accessible to another through weakly isolated queries, a pattern that aligns with BOLA/IDOR findings that middleBrick checks across DynamoDB workflows.
Dynamodb-Specific Remediation in Echo Go — concrete code fixes
Remediation centers on minimizing the data footprint stored in DynamoDB and controlling what is returned to Echo responses. Use selective field structures for persistence, avoid storing raw request bodies directly, and always limit returned attributes with projection expressions.
- Separate persistence models from API models: define a
UserInputfor binding and aUserRecordfor DynamoDB that excludes PII unless explicitly required. - Use
attributevalue.MarshalMaponly on filtered structs and preferav.MarshalListOfMapsfor partial updates. - Always specify
ProjectionExpressionon queries and scans, and useFilterExpressiononly for post-retrieval filtering (not security). - Encrypt PII at rest by storing only tokens or encrypted blobs; manage keys outside DynamoDB.
- Audit index key schemas to ensure PII is not included in GSIs unless strictly necessary.
Secure Echo handler example with DynamoDB:
// Define request-specific model (bind only what you need)
type UserInput struct {
UserID string `json:"userId" validate:"required"`
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
// Define a DynamoDB-specific model that omits or encrypts PII
type UserRecord struct {
PK string `json:"pk"`
SK string `json:"sk"`
Name string `json:"name"`
EmailTok string `json:"email_tok"` // tokenized or encrypted
Created string `json:"created"`
}
// Echo handler: bind to input model, map to record, write with limited attributes
func UpdateProfile(c echo.Context) error {
var in UserInput
if err := c.Bind(&in); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid payload"})
}
// Transform to DynamoDB model (apply business rules, tokenization, encryption here)
rec := UserRecord{
PK: "USER#" + in.UserID,
SK: "PROFILE",
Name: in.Name,
EmailTok: tokenize(in.Email), // placeholder for your tokenization/encryption
Created: time.Now().UTC().Format(time.RFC3339),
}
av, err := attributevalue.MarshalMap(rec)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
input := &dynamodb.PutItemInput{
TableName: aws.String("users"),
Item: av,
ConditionExpression: aws.String("attribute_not_exists(PK)"),
ReturnConsumedCapacity: aws.String("NONE"),
}
_, err = svc.PutItem(context.TODO(), input)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusOK)
}
Query with controlled attribute retrieval:
out, err := svc.Query(context.TODO(), &dynamodb.QueryInput{
TableName: aws.String("users"),
KeyConditionExpression: aws.String("pk = :pk"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "USER#123"},
},
ProjectionExpression: aws.String("pk, sk, name, email_tok"), // limit returned fields
})
if err != nil {
return err
}
var items []UserRecord
if err := attributevalue.UnmarshalListOfMaps(out.Items, &items); err != nil {
return err
}
For scans that must traverse the table, avoid returning PII by specifying ProjectionExpression and filtering on the server side where possible. If PII must be stored, ensure it is encrypted before writing and decrypted only in secure contexts, and avoid logging raw responses in Echo middleware or error handlers.
middleBrick scans can surface PII exposure by checking whether responses include sensitive attributes without projection, whether GSIs include PII in keys, and whether unauthenticated endpoints return items containing PII. These findings map to OWASP API Top 10 and compliance frameworks to guide remediation.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |