Header Injection in Aspnet with Dynamodb
Header Injection in Aspnet with Dynamodb — how this specific combination creates or exposes the vulnerability
Header Injection occurs when user-controlled data is reflected into HTTP response headers without validation or encoding. In an ASP.NET application that queries DynamoDB, this typically happens when developers take request inputs (e.g., query parameters, headers, or cookies) and use them to construct DynamoDB attribute values or conditional expressions, then inadvertently expose data or control header construction based on DynamoDB results.
Consider an endpoint that retrieves a user profile from DynamoDB and sets a custom header based on a returned attribute:
// ASP.NET Core controller example that combines user input, DynamoDB, and header reflection
[ApiController]
[Route("api/[controller]")]
public class ProfileController : ControllerBase
{
private readonly IAmazonDynamoDB _dynamoDb;
public ProfileController(IAmazonDynamoDB dynamoDb)
{
_dynamoDb = dynamoDb;
}
[HttpGet("{userId}")]
public async Task GetProfile(string userId)
{
var request = new GetItemRequest
{
TableName = "UserProfiles",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = userId } }
}
};
var response = await _dynamoDb.GetItemAsync(request);
if (response.Item != null && response.Item.TryGetValue("DisplayName", out var attr))
{
// Vulnerable: user-controlled data from DynamoDB used in header
Response.Headers["X-Display-Name"] = attr.S;
}
return Ok(response.Item);
}
}
If the DisplayName attribute stored in DynamoDB contains newline or carriage return characters (e.g., due to improper input validation when the attribute was written), an attacker can supply a userId that causes header injection. For example, a stored value like John\r\nX-Admin: true would split the response and inject an additional header when reflected. Even when the header is not directly reflected, DynamoDB-stored data used downstream to decide which headers to set can create logic-based header injection, such as conditionally adding security headers only when certain attributes are present.
The DynamoDB-specific aspect is that the database can store multi-line strings or unexpected characters if input validation was enforced at write time only or if data came from an untrusted source. Because DynamoDB does not enforce a strict schema, application-level validation is essential. An attacker who can influence what gets stored (e.g., through another API or compromised admin interface) can later trigger header injection via read paths like the one above.
Additionally, DynamoDB conditional writes or expressions that incorporate user-controlled values can be abused if the outcome influences header-related behavior. For instance, using a ConditionExpression that checks an attribute value to decide whether to return data, and then using that decision to set headers, may allow an attacker to manipulate header presence based on injected attribute values.
Dynamodb-Specific Remediation in Aspnet — concrete code fixes
Remediation focuses on input validation, output encoding, and strict separation of data and control flow. Never trust data stored in DynamoDB; treat all attribute values as untrusted when used in HTTP headers.
1. Validate and sanitize data at write time
Ensure that attributes intended for header use do not contain control characters. Reject or encode newline and carriage return characters when storing user-controlled data in DynamoDB.
public static bool IsValidHeaderValue(string value)
{
// Reject newlines and carriage returns which enable header injection
return !string.IsNullOrEmpty(value) && !value.Contains("\r") && !value.Contains("\n");
}
// Usage when writing to DynamoDB
var putRequest = new PutItemRequest
{
TableName = "UserProfiles",
Item = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = userId } },
{ "DisplayName", new AttributeValue { S = SanitizeDisplayName(inputName) } }
}
};
string SanitizeDisplayName(string input)
{
if (!IsValidHeaderValue(input)) throw new ArgumentException("Invalid display name");
return input;
}
2. Encode or reject dynamic values when setting headers
When using DynamoDB data in headers, either reject values containing control characters or encode them to prevent injection. For headers that should not contain newlines, drop or reject the header rather than attempting to sanitize.
[HttpGet("{userId}")]
public async Task<IActionResult> GetProfile(string userId)
{
var request = new GetItemRequest
{
TableName = "UserProfiles",
Key = new Dictionary<string, AttributeValue>
{
{ "UserId", new AttributeValue { S = userId } }
}
};
var response = await _dynamoDb.GetItemAsync(request);
if (response.Item != null && response.Item.TryGetValue("DisplayName", out var attr))
{
var displayName = attr.S;
// Reject or skip header if value contains control characters
if (string.IsNullOrEmpty(displayName) || displayName.Contains("\r") || displayName.Contains("\n"))
{
// Optionally log and skip setting the header
return BadRequest("Invalid display name");
}
Response.Headers["X-Display-Name"] = displayName;
}
return Ok(response.Item);
}
3. Use a strict allowlist for header names and avoid dynamic header keys
Never use DynamoDB data to construct header names. Only set known, static header names and validate any dynamic values against an allowlist of safe characters (e.g., alphanumeric, hyphen, underscore). If you must store display names, keep them in DynamoDB but transform them before header assignment (e.g., hash or truncate), or use a controlled mapping.
// Safe pattern: static header name, validated value only
Response.Headers["X-User-Name"] = "Profile"; // static
// Do NOT do: Response.Headers[attr.S] = "value";
4. Prefer explicit mappings over dynamic header derivation from DynamoDB
If the presence or value of a header must depend on stored attributes, map DynamoDB attributes to a controlled set of rules in code rather than reflecting them directly. This prevents unexpected values from controlling HTTP protocol elements.
var role = response.Item?.TryGetValue("Role", out var roleAttr) == true ? roleAttr.S : "user";
switch (role)
{
case "admin":
Response.Headers["X-Access-Level"] = "admin";
break;
case "guest":
Response.Headers["X-Access-Level"] = "guest";
break;
default:
Response.Headers["X-Access-Level"] = "user";
break;
}