Incomplete Model of Endpoint Features
Description
Incomplete Model of Endpoint Features is a vulnerability where a product acts as an intermediary or monitor between two or more endpoints, but it does not have a complete model of an endpoint's features, behaviors, or state. This incomplete understanding causes the product to perform incorrect actions based on flawed assumptions about how endpoints will interpret or process data. The weakness commonly affects security-critical intermediaries such as firewalls, proxies, intrusion detection systems, and antivirus gateways that must understand diverse endpoint behaviors to function correctly.
Risk
Incomplete endpoint modeling creates severe security gaps in intermediary systems. Attackers exploit interpretation differences between the intermediary and actual endpoints to bypass security controls, smuggle malicious content, or evade detection. HTTP request smuggling attacks leverage differences in how proxies and backend servers parse HTTP headers. Email gateway bypasses occur when antivirus products misjudge how client applications handle attachments. Web application firewalls fail when they don't model browser quirks attackers exploit. These vulnerabilities are particularly dangerous because they undermine security infrastructure that organizations rely upon for defense.
Solution
Build comprehensive models of endpoint behaviors including edge cases and version-specific quirks. Implement strict parsing that rejects ambiguous or malformed data rather than guessing intent. Use canonicalization to normalize data before processing. When multiple interpretations are possible, choose the most restrictive interpretation or reject the data entirely. Keep endpoint behavior models updated as new versions introduce changes. Consider defense in depth where multiple independent security layers each model endpoints differently. Test intermediary behavior against actual endpoint implementations to verify assumptions.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Unexpected State - Intermediary makes incorrect decisions based on flawed understanding of endpoint behavior, allowing malicious content or requests to pass through security controls. |
| Access Control | Scope: Access Control Bypass Protection Mechanism - Attackers can bypass security intermediaries by exploiting differences between how the intermediary and the endpoint interpret data. |
Example Code
Vulnerable Code
# Vulnerable: HTTP proxy with incomplete request parsing model
class VulnerableHTTPProxy:
def parse_request(self, raw_request):
# Vulnerable: Only handles standard Content-Length
headers = {}
lines = raw_request.split('\r\n')
for line in lines[1:]:
if ': ' in line:
key, value = line.split(': ', 1)
headers[key] = value
# Vulnerable: Doesn't handle Transfer-Encoding
# Backend server may use chunked encoding while proxy uses Content-Length
content_length = int(headers.get('Content-Length', 0))
return {
'method': lines[0].split()[0],
'path': lines[0].split()[1],
'headers': headers,
'body_length': content_length
}
def forward_request(self, request, raw_data):
# Vulnerable: Request smuggling possible
# Proxy sees one request, backend sees two
# Attacker sends:
# POST / HTTP/1.1
# Content-Length: 4
# Transfer-Encoding: chunked
#
# 0
#
# GET /admin HTTP/1.1
# Proxy uses Content-Length (4 bytes), thinks body is "0\r\n\r"
# Backend uses Transfer-Encoding chunked, sees end of first request
# Then processes "GET /admin" as second request
backend_response = self.backend.send(raw_data)
return backend_response
// Vulnerable: Email gateway with incomplete attachment model
public class VulnerableMailGateway {
public boolean scanAttachment(MimeBodyPart attachment) throws Exception {
String filename = attachment.getFileName();
String contentType = attachment.getContentType();
// Vulnerable: Only checks declared content type
// Doesn't model how different email clients handle attachments
if (contentType.startsWith("text/plain")) {
// Assumes plain text is safe
return true;
}
// Vulnerable: Extension-based checking
// Different clients may execute based on different criteria
if (filename.endsWith(".txt")) {
return true; // Considered safe
}
// Vulnerable: Doesn't account for:
// - Outlook executing .txt.exe (hidden extensions)
// - Clients using MIME type to override extension
// - Character encoding tricks (RTL override)
// - Zone identifier behavior on Windows
return scanWithAntivirus(attachment);
}
// Vulnerable: Incomplete model of content inspection
public boolean inspectContent(InputStream content) {
// Only scans first 1MB
// Endpoint may process entire file
byte[] sample = new byte[1024 * 1024];
int read = content.read(sample);
return !containsMaliciousSignature(sample);
}
}
// Vulnerable: Firewall with incomplete protocol model
#include <stdio.h>
#include <string.h>
typedef struct {
char *data;
size_t length;
} HttpRequest;
// Vulnerable: Incomplete HTTP parsing
int firewall_inspect_http(const char *packet, size_t len) {
// Vulnerable: Only looks for standard patterns
// Doesn't model HTTP/0.9, HTTP/2, or edge cases
if (strncmp(packet, "GET ", 4) == 0 ||
strncmp(packet, "POST ", 5) == 0) {
// Vulnerable: Simple URL extraction
char *path_start = strchr(packet, ' ') + 1;
char *path_end = strchr(path_start, ' ');
size_t path_len = path_end - path_start;
char path[1024];
strncpy(path, path_start, path_len);
path[path_len] = '\0';
// Vulnerable: Doesn't handle URL encoding variants
// Backend may decode %2e%2e as ".." while firewall doesn't
if (strstr(path, "../") != NULL) {
return BLOCK; // Path traversal
}
// Vulnerable: Doesn't model backend's URL parsing
// /admin;bypass.jpg may be blocked
// But /admin%3bbypass.jpg passes firewall, decoded by backend
if (strstr(path, "/admin") != NULL) {
return BLOCK;
}
}
return ALLOW;
}
Fixed Code
# Fixed: HTTP proxy with comprehensive request parsing
class SecureHTTPProxy:
def parse_request(self, raw_request):
headers = {}
lines = raw_request.split('\r\n')
for line in lines[1:]:
if ': ' in line:
key, value = line.split(': ', 1)
# Fixed: Normalize header names
key = key.lower().strip()
headers[key] = value.strip()
# Fixed: Detect ambiguous requests
has_content_length = 'content-length' in headers
has_transfer_encoding = 'transfer-encoding' in headers
# Fixed: Reject ambiguous requests per RFC 7230
if has_content_length and has_transfer_encoding:
raise AmbiguousRequestError(
"Request contains both Content-Length and Transfer-Encoding"
)
# Fixed: Detect smuggling attempts
if has_transfer_encoding:
te_value = headers['transfer-encoding'].lower()
# Fixed: Reject malformed Transfer-Encoding
if te_value != 'chunked':
raise InvalidRequestError(
f"Invalid Transfer-Encoding: {te_value}"
)
return self._normalize_request(lines, headers)
def forward_request(self, request, raw_data):
# Fixed: Re-serialize request to ensure consistency
normalized = self._serialize_request(request)
# Fixed: Use strict parsing for response too
backend_response = self.backend.send(normalized)
# Fixed: Validate response integrity
self._validate_response(backend_response)
return backend_response
def _serialize_request(self, request):
# Fixed: Create unambiguous request
# Only use Content-Length for requests with bodies
lines = [f"{request['method']} {request['path']} HTTP/1.1"]
for key, value in request['headers'].items():
if key.lower() not in ['transfer-encoding', 'content-length']:
lines.append(f"{key}: {value}")
if request.get('body'):
lines.append(f"Content-Length: {len(request['body'])}")
return '\r\n'.join(lines) + '\r\n\r\n' + request.get('body', '')
// Fixed: Email gateway with comprehensive attachment model
public class SecureMailGateway {
private static final Map<String, Set<String>> SAFE_TYPES = Map.of(
"text/plain", Set.of("txt", "csv"),
"image/jpeg", Set.of("jpg", "jpeg"),
"image/png", Set.of("png")
);
public boolean scanAttachment(MimeBodyPart attachment) throws Exception {
String filename = attachment.getFileName();
String declaredType = attachment.getContentType();
// Fixed: Detect dangerous filename tricks
if (containsRtlOverride(filename) || hasHiddenExtension(filename)) {
log.warn("Suspicious filename detected: {}", filename);
return false;
}
// Fixed: Verify actual content type matches declaration
byte[] content = readAllBytes(attachment.getInputStream());
String actualType = detectContentType(content);
// Fixed: Content type must match declared type
if (!typesMatch(declaredType, actualType)) {
log.warn("Content type mismatch: declared={}, actual={}",
declaredType, actualType);
return false;
}
// Fixed: Extension must match content type
String extension = getExtension(filename);
if (!extensionMatchesType(extension, actualType)) {
log.warn("Extension/type mismatch: ext={}, type={}",
extension, actualType);
return false;
}
// Fixed: Scan entire content, not just sample
return scanWithAntivirus(content);
}
private boolean containsRtlOverride(String filename) {
// Fixed: Detect Unicode tricks
return filename.contains("\u202E") || // RTL Override
filename.contains("\u200B") || // Zero-width space
filename.contains("\u00A0"); // Non-breaking space
}
private boolean hasHiddenExtension(String filename) {
// Fixed: Detect double extensions that hide true type
String[] dangerousExts = {".exe", ".bat", ".cmd", ".ps1", ".vbs", ".js"};
String lowerName = filename.toLowerCase();
for (String ext : dangerousExts) {
if (lowerName.contains(ext + ".")) {
return true; // e.g., "file.exe.txt"
}
}
return false;
}
}
// Fixed: Firewall with comprehensive protocol model
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// Fixed: Comprehensive URL decoding
char *url_decode(const char *src, size_t len) {
char *decoded = malloc(len + 1);
char *dst = decoded;
for (size_t i = 0; i < len; i++) {
if (src[i] == '%' && i + 2 < len &&
isxdigit(src[i+1]) && isxdigit(src[i+2])) {
// Fixed: Decode percent-encoded characters
char hex[3] = {src[i+1], src[i+2], '\0'};
*dst++ = (char)strtol(hex, NULL, 16);
i += 2;
} else if (src[i] == '+') {
*dst++ = ' ';
} else {
*dst++ = src[i];
}
}
*dst = '\0';
return decoded;
}
// Fixed: Normalize path for consistent comparison
char *normalize_path(const char *path) {
// Fixed: Decode URL encoding first
char *decoded = url_decode(path, strlen(path));
// Fixed: Resolve path traversal sequences
char *normalized = resolve_path_traversal(decoded);
// Fixed: Lowercase for case-insensitive comparison
for (char *p = normalized; *p; p++) {
*p = tolower(*p);
}
free(decoded);
return normalized;
}
int firewall_inspect_http(const char *packet, size_t len) {
HttpRequest req;
// Fixed: Use comprehensive HTTP parser
int parse_result = parse_http_strict(packet, len, &req);
if (parse_result != PARSE_OK) {
// Fixed: Reject malformed requests
return BLOCK;
}
// Fixed: Normalize path before inspection
char *normalized_path = normalize_path(req.path);
// Fixed: Check against normalized path
if (contains_path_traversal(normalized_path)) {
free(normalized_path);
return BLOCK;
}
// Fixed: Use normalized comparison
if (strstr(normalized_path, "/admin") != NULL) {
free(normalized_path);
return BLOCK;
}
// Fixed: Also check for bypass techniques
// Remove null bytes, semicolons, etc. that backends may interpret
if (contains_suspicious_chars(req.path)) {
free(normalized_path);
return BLOCK;
}
free(normalized_path);
return ALLOW;
}
CVE Examples
- CVE-2005-1992 - HTTP request smuggling via differences in how proxy and backend server interpret Transfer-Encoding headers.
- CVE-2020-8287 - Node.js HTTP parser allowed header smuggling due to incomplete modeling of HTTP specification.
- CVE-2021-21295 - Netty HTTP request smuggling through different interpretations of Content-Length.
References
- MITRE Corporation. "CWE-437: Incomplete Model of Endpoint Features." https://cwe.mitre.org/data/definitions/437.html
- PortSwigger. "HTTP Request Smuggling." https://portswigger.net/web-security/request-smuggling