Missing Password Field Masking

Description

Missing Password Field Masking is a vulnerability where an application does not mask password input during entry, allowing the password characters to be visible on screen. When password fields display entered characters instead of masking them with dots or asterisks, anyone within visual range can observe and capture the password. This includes shoulder surfing attacks in public spaces, recorded screen sharing sessions, screenshots, and video recordings of user sessions.

Risk

Visible password entry creates significant credential exposure risks. Shoulder surfing in public areas like cafes, airports, or open offices allows nearby observers to capture passwords. Screen recording malware can capture visible credentials during entry. Screen sharing sessions during support calls or presentations may inadvertently expose passwords. Screenshots taken for troubleshooting may include visible passwords. Security cameras or surveillance footage can capture password entry. Mobile devices used in public are particularly vulnerable due to smaller screens and closer viewing distances by others.

Solution

Implement password masking using appropriate input type attributes (type="password" in HTML). Provide an optional "show password" toggle that temporarily reveals characters, with clear visual indication when password is visible. Ensure password masking is applied consistently across all password entry forms including login, registration, password change, and password reset. Test on all supported platforms and browsers to ensure masking works correctly. For accessibility, provide alternative authentication methods or screen reader compatible password entry. Consider implementing paste functionality for password managers while maintaining visual masking.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Visible password characters allow unauthorized observers to capture credentials, enabling them to bypass authentication and gain unauthorized system access.

Example Code

Vulnerable Code

<!-- Vulnerable: Password field without masking -->
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <form action="/login" method="POST">
        <label>Username:</label>
        <input type="text" name="username">

        <!-- Vulnerable: Using type="text" for password -->
        <label>Password:</label>
        <input type="text" name="password">
        <!-- Password visible as user types! -->

        <button type="submit">Login</button>
    </form>
</body>
</html>
<!-- Vulnerable: Custom password field without masking -->
<form action="/register" method="POST">
    <label>Create Password:</label>
    <!-- Vulnerable: Custom input styling but no masking -->
    <input type="text" name="password" class="password-input"
           placeholder="Enter password">

    <label>Confirm Password:</label>
    <!-- Vulnerable: Both password fields show characters -->
    <input type="text" name="confirm_password" class="password-input"
           placeholder="Confirm password">
</form>

<style>
/* Vulnerable: Trying to "secure" with styling doesn't mask input */
.password-input {
    font-family: 'password-font';  /* Doesn't actually mask */
    letter-spacing: 2px;
}
</style>
// Vulnerable: React component with unmasked password
import React, { useState } from 'react';

function VulnerableLoginForm() {
    const [password, setPassword] = useState('');

    return (
        <form onSubmit={handleSubmit}>
            <label>Password:</label>
            {/* Vulnerable: type="text" instead of "password" */}
            <input
                type="text"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
            />
            <button type="submit">Login</button>
        </form>
    );
}

// Vulnerable: Custom password input that doesn't mask
function VulnerablePasswordInput({ value, onChange }) {
    return (
        <div className="password-container">
            {/* Vulnerable: Visible text input */}
            <input
                type="text"
                value={value}
                onChange={onChange}
                autoComplete="off"  // Doesn't help with visibility
            />
        </div>
    );
}
// Vulnerable: iOS password field without secure entry
import UIKit

class VulnerableLoginViewController: UIViewController {
    let passwordField = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Vulnerable: isSecureTextEntry not set
        passwordField.placeholder = "Enter password"
        passwordField.borderStyle = .roundedRect
        // Missing: passwordField.isSecureTextEntry = true

        view.addSubview(passwordField)
    }
}

// Vulnerable: SwiftUI without secure field
import SwiftUI

struct VulnerableLoginView: View {
    @State private var password = ""

    var body: some View {
        VStack {
            // Vulnerable: Using TextField instead of SecureField
            TextField("Password", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }
    }
}
// Vulnerable: Android password field without masking
import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity

class VulnerableLoginActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val passwordField = EditText(this)
        passwordField.hint = "Enter password"

        // Vulnerable: No input type set for password masking
        // Missing: passwordField.inputType = InputType.TYPE_CLASS_TEXT or
        //                                    InputType.TYPE_TEXT_VARIATION_PASSWORD

        setContentView(passwordField)
    }
}
<!-- Vulnerable: Android XML layout without password input type -->
<EditText
    android:id="@+id/password_field"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Password"
    android:inputType="text" />
    <!-- Vulnerable: Should be textPassword -->

Fixed Code

<!-- Fixed: Proper password masking with optional show/hide -->
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
    <style>
        .password-container {
            position: relative;
            display: inline-block;
        }
        .toggle-password {
            position: absolute;
            right: 10px;
            top: 50%;
            transform: translateY(-50%);
            cursor: pointer;
            background: none;
            border: none;
        }
    </style>
</head>
<body>
    <form action="/login" method="POST">
        <label>Username:</label>
        <input type="text" name="username" autocomplete="username">

        <label>Password:</label>
        <div class="password-container">
            <!-- Fixed: type="password" for masking -->
            <input type="password" name="password" id="password"
                   autocomplete="current-password">
            <!-- Fixed: Optional toggle with clear indication -->
            <button type="button" class="toggle-password"
                    onclick="togglePasswordVisibility()"
                    aria-label="Toggle password visibility">
                <span id="toggle-icon">👁️</span>
            </button>
        </div>

        <button type="submit">Login</button>
    </form>

    <script>
        function togglePasswordVisibility() {
            const passwordInput = document.getElementById('password');
            const toggleIcon = document.getElementById('toggle-icon');

            if (passwordInput.type === 'password') {
                passwordInput.type = 'text';
                toggleIcon.textContent = '🔒';
                // Fixed: Auto-hide after a few seconds for security
                setTimeout(() => {
                    passwordInput.type = 'password';
                    toggleIcon.textContent = '👁️';
                }, 3000);
            } else {
                passwordInput.type = 'password';
                toggleIcon.textContent = '👁️';
            }
        }
    </script>
</body>
</html>
<!-- Fixed: Registration form with masked password fields -->
<form action="/register" method="POST">
    <label for="new-password">Create Password:</label>
    <div class="password-container">
        <!-- Fixed: Proper password input type -->
        <input type="password" id="new-password" name="password"
               autocomplete="new-password"
               minlength="12"
               required>
    </div>

    <label for="confirm-password">Confirm Password:</label>
    <div class="password-container">
        <!-- Fixed: Confirmation field also masked -->
        <input type="password" id="confirm-password" name="confirm_password"
               autocomplete="new-password"
               minlength="12"
               required>
    </div>

    <button type="submit">Create Account</button>
</form>
// Fixed: React component with properly masked password
import React, { useState } from 'react';

function SecureLoginForm() {
    const [password, setPassword] = useState('');
    const [showPassword, setShowPassword] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        // Handle login...
    };

    return (
        <form onSubmit={handleSubmit}>
            <label htmlFor="password">Password:</label>
            <div className="password-container">
                {/* Fixed: Dynamic type based on visibility toggle */}
                <input
                    id="password"
                    type={showPassword ? 'text' : 'password'}
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    autoComplete="current-password"
                />
                <button
                    type="button"
                    onClick={() => setShowPassword(!showPassword)}
                    aria-label={showPassword ? 'Hide password' : 'Show password'}
                >
                    {showPassword ? '🔒' : '👁️'}
                </button>
            </div>
            <button type="submit">Login</button>
        </form>
    );
}

// Fixed: Reusable secure password input component
function SecurePasswordInput({ value, onChange, id, label, autoComplete }) {
    const [visible, setVisible] = useState(false);

    // Fixed: Auto-hide after timeout when shown
    React.useEffect(() => {
        if (visible) {
            const timer = setTimeout(() => setVisible(false), 3000);
            return () => clearTimeout(timer);
        }
    }, [visible]);

    return (
        <div className="password-field">
            <label htmlFor={id}>{label}</label>
            <div className="input-wrapper">
                <input
                    id={id}
                    type={visible ? 'text' : 'password'}
                    value={value}
                    onChange={onChange}
                    autoComplete={autoComplete}
                />
                <button
                    type="button"
                    onClick={() => setVisible(!visible)}
                    aria-pressed={visible}
                    aria-label="Toggle password visibility"
                >
                    {visible ? 'Hide' : 'Show'}
                </button>
            </div>
        </div>
    );
}

export { SecureLoginForm, SecurePasswordInput };
// Fixed: iOS password field with secure entry
import UIKit

class SecureLoginViewController: UIViewController {
    let passwordField = UITextField()
    let toggleButton = UIButton()
    var isPasswordVisible = false

    override func viewDidLoad() {
        super.viewDidLoad()

        // Fixed: Enable secure text entry
        passwordField.placeholder = "Enter password"
        passwordField.borderStyle = .roundedRect
        passwordField.isSecureTextEntry = true
        passwordField.textContentType = .password

        // Fixed: Add visibility toggle
        toggleButton.setTitle("Show", for: .normal)
        toggleButton.addTarget(self, action: #selector(togglePasswordVisibility),
                              for: .touchUpInside)

        view.addSubview(passwordField)
        view.addSubview(toggleButton)
    }

    @objc func togglePasswordVisibility() {
        isPasswordVisible.toggle()
        passwordField.isSecureTextEntry = !isPasswordVisible
        toggleButton.setTitle(isPasswordVisible ? "Hide" : "Show", for: .normal)

        // Fixed: Auto-hide after delay
        if isPasswordVisible {
            DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in
                self?.passwordField.isSecureTextEntry = true
                self?.isPasswordVisible = false
                self?.toggleButton.setTitle("Show", for: .normal)
            }
        }
    }
}

// Fixed: SwiftUI with SecureField
import SwiftUI

struct SecureLoginView: View {
    @State private var password = ""
    @State private var isPasswordVisible = false

    var body: some View {
        VStack {
            HStack {
                // Fixed: Use SecureField for masked input
                if isPasswordVisible {
                    TextField("Password", text: $password)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                } else {
                    SecureField("Password", text: $password)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                }

                Button(action: { isPasswordVisible.toggle() }) {
                    Image(systemName: isPasswordVisible ? "eye.slash" : "eye")
                }
            }
        }
    }
}
// Fixed: Android password field with masking
import android.os.Bundle
import android.text.InputType
import android.text.method.PasswordTransformationMethod
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity

class SecureLoginActivity : AppCompatActivity() {
    private var isPasswordVisible = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val layout = LinearLayout(this).apply {
            orientation = LinearLayout.HORIZONTAL
        }

        val passwordField = EditText(this).apply {
            hint = "Enter password"
            // Fixed: Set password input type for masking
            inputType = InputType.TYPE_CLASS_TEXT or
                       InputType.TYPE_TEXT_VARIATION_PASSWORD
        }

        val toggleButton = ImageButton(this).apply {
            setImageResource(android.R.drawable.ic_menu_view)
            setOnClickListener {
                isPasswordVisible = !isPasswordVisible
                if (isPasswordVisible) {
                    // Show password
                    passwordField.transformationMethod = null
                } else {
                    // Hide password
                    passwordField.transformationMethod =
                        PasswordTransformationMethod.getInstance()
                }
                // Move cursor to end
                passwordField.setSelection(passwordField.text.length)
            }
        }

        layout.addView(passwordField)
        layout.addView(toggleButton)
        setContentView(layout)
    }
}
<!-- Fixed: Android XML layout with password input type -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <!-- Fixed: inputType includes textPassword for masking -->
    <EditText
        android:id="@+id/password_field"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Password"
        android:inputType="textPassword"
        android:autofillHints="password" />

    <ImageButton
        android:id="@+id/toggle_password"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_visibility"
        android:contentDescription="Toggle password visibility" />

</LinearLayout>

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, missing password masking is a common finding in:

  • Security assessments of web applications
  • Mobile application security reviews
  • Compliance audits (PCI-DSS, HIPAA)

References

  1. MITRE Corporation. "CWE-549: Missing Password Field Masking." https://cwe.mitre.org/data/definitions/549.html
  2. OWASP. "Authentication Cheat Sheet."
  3. W3C. "HTML5 input types - password."