Skip to content

CWE-501: Trust Boundary Violation

Overview

Trust boundary violations occur when untrusted data (user input, HTTP request data) is stored in trusted context (session, internal objects) without validation, or when trusted data is exposed to untrusted context, enabling session poisoning, privilege escalation, and security control bypass.

OWASP Classification

A06:2025 - Insecure Design

Risk

High: Trust violations enable session poisoning (storing attacker-controlled data in session), privilege escalation (manipulating stored roles), cache poisoning, bypassing security checks, and violating security assumptions throughout application.

Remediation Steps

Core principle: Treat trust boundaries explicitly; never let data cross boundaries without validation, authorization, and least privilege.

Locate Trust Boundary Violations

When reviewing security scan results:

  • Examine data_paths: Identify where untrusted data crosses into trusted context
  • Find trust boundaries: Session storage, caches, internal objects, security contexts
  • Check for validation: Look for missing validation before storing in trusted contexts
  • Identify trust assumptions: Code that assumes session/cache data is safe
  • Assess impact: Can attacker control session variables, roles, or security decisions

Common trust boundaries:

  • HTTP request → Session storage
  • User input → Application cache
  • External data → Internal security context
  • Untrusted parameters → Authorization decisions

Validate Before Crossing Trust Boundary (Primary Defense)

# WRONG - storing untrusted data directly in session
@app.route('/set_preference')
def set_pref():
    pref = request.args.get('pref')
    session['user_pref'] = pref  # Trust violation!
    # Attacker can set: ?pref=<script>alert(1)</script>

# CORRECT - validate before storing in trusted context
ALLOWED_PREFS = ['light', 'dark', 'auto']

@app.route('/set_preference')
def set_pref():
    pref = request.args.get('pref')

    # Validate against allowlist
    if pref not in ALLOWED_PREFS:
        abort(400, "Invalid preference")

    # Now safe to store in session
    session['user_pref'] = pref  # Trusted after validation

Why this works: Validating data before crossing the trust boundary ensures only known-good values enter trusted contexts. Allowlisting is the strongest validation approach.

Validation strategies:

  • Use allowlists for enumerated values
  • Type validation and conversion
  • Length and format checks
  • Sanitization for display data
  • Never trust user input implicitly

Never Store Untrusted Data in Session Without Validation

// WRONG - session poisoning vulnerability
HttpSession session = request.getSession();
String userInput = request.getParameter("data");
session.setAttribute("userData", userInput);  // Violation!

String role = request.getParameter("role");
session.setAttribute("role", role);  // Privilege escalation!

// CORRECT - only store validated/sanitized data
String userInput = request.getParameter("data");

// Validate and sanitize
String sanitized = SecurityUtils.sanitizeInput(userInput);
if (!SecurityUtils.isValid(sanitized, MAX_LENGTH)) {
    throw new ValidationException("Invalid input");
}

// Safe to store after validation
session.setAttribute("userData", sanitized);

// For security-critical data, NEVER accept from user
// Get role from database based on authenticated user ID
User user = userService.getAuthenticatedUser();
session.setAttribute("role", user.getRole());  // From trusted source

Session security rules:

  • Never store user-provided roles/permissions
  • Validate all user input before session storage
  • Separate display preferences from security decisions
  • Re-validate session data when making security decisions

Separate Trusted and Untrusted Data Structures

class UserContext:
    def __init__(self, user_id, role):
        # TRUSTED - from authentication/database
        self._user_id = user_id  # Private, not settable
        self._role = role  # Private, not settable
        self._authenticated_at = datetime.now()

        # UNTRUSTED - from user, kept separate
        self.preferences = {}  # User preferences
        self.display_settings = {}  # Display settings

    @property
    def user_id(self):
        return self._user_id  # Read-only access

    @property
    def role(self):
        return self._role  # Read-only access

    def set_preference(self, key, value):
        # Validate untrusted data before storing
        if key not in ALLOWED_PREFERENCE_KEYS:
            raise ValueError(f"Invalid preference key: {key}")

        if not self._validate_preference_value(key, value):
            raise ValueError(f"Invalid value for {key}")

        self.preferences[key] = value

    def _validate_preference_value(self, key, value):
        # Type and range validation
        if key == 'theme':
            return value in ['light', 'dark', 'auto']
        elif key == 'language':
            return value in SUPPORTED_LANGUAGES
        return False

Separation principles:

  • Use private fields for trusted data
  • Provide read-only access to sensitive fields
  • Keep untrusted data in separate containers
  • Apply validation at the boundary

Validate When Reading from Trusted Contexts

# Don't blindly trust session data - it could be poisoned
def get_user_role():
    role = session.get('role')

    # Re-validate even from session (defense in depth)
    VALID_ROLES = ['user', 'admin', 'moderator']
    if role not in VALID_ROLES:
        # Session might be poisoned or corrupted
        logger.warning(f"Invalid role in session: {role}")
        clear_session()
        raise SecurityException("Invalid session data")

    return role

# Better: lookup from database, don't rely on session
def get_user_role_secure():
    user_id = session.get('user_id')
    if not user_id:
        raise AuthenticationException("Not authenticated")

    # Authoritative source from database
    user = db.query(User).get(user_id)
    if not user:
        raise AuthenticationException("User not found")

    return user.role

Defense-in-depth validation:

  • Don't assume session data is safe
  • Re-validate before security decisions
  • Use database as authoritative source for critical data
  • Clear session if data is invalid

Monitor and Test for Trust Boundary Violations

Testing strategies:

# Test session poisoning
def test_cannot_set_admin_role():
    # Attempt to poison session with admin role
    response = client.post('/set_preference', 
                          data={'pref': 'admin_role'})

    # Should reject invalid preference
    assert response.status_code == 400

    # Session should not contain attacker-controlled role
    with client.session_transaction() as sess:
        assert 'admin_role' not in sess
        assert sess.get('role') != 'admin'

# Test cache poisoning
def test_cache_validation():
    # Try to inject malicious data into cache
    malicious_data = '<script>alert(1)</script>'
    response = client.post('/update', data={'value': malicious_data})

    # Verify data is sanitized before caching
    cached = cache.get('user_data')
    assert '<script>' not in cached

Attack simulation:

  • Modify URL parameters to inject admin roles
  • Tamper with form data before submission
  • Use proxy to modify requests (Burp Suite)
  • Test with XSS payloads in session data
  • Attempt privilege escalation via session manipulation

Monitoring:

  • Log validation failures at trust boundaries
  • Alert on invalid session data detected
  • Monitor for session/cache poisoning attempts
  • Track unexpected role changes
  • Audit security-critical session operations

Common Violation Patterns

# Storing unvalidated input in session

session['redirect_url'] = request.args.get('next')  # Open redirect!
session['lang'] = request.args.get('lang')  # Could be malicious

# Storing attacker-controlled data

user_data = request.json
cache.set(f'user_{user_id}', user_data)  # Cache poisoning!

# Mixing trust levels

class User:
    def __init__(self):
        self.id = None  # Trusted (from DB)
        self.display_name = None  # Untrusted (user input)
        # Dangerous - mixed trust levels

Attack Scenarios

# Session poisoning
# Attacker sets: ?role=admin

session['role'] = request.args.get('role')  # Violation!

# Later
if session['role'] == 'admin':  # Attacker is now admin!
    allow_admin_action()

# Cache poisoning
user_input = request.args.get('data')
cache.set(key, user_input)  # Untrusted data in cache

# Other users get attacker's data
# Object injection
session['user_object'] = pickle.loads(request.data)  # RCE!

Security Checklist

  • All user input validated before session storage
  • Security-critical data (roles, permissions) from database, not user
  • Trusted and untrusted data kept separate
  • Session data re-validated before security decisions
  • Allowlists used for enumerated values
  • Input sanitized for display contexts

Additional Resources