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
| Impact | Details |
|---|---|
| Access Control | Scope: 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
- MITRE Corporation. "CWE-549: Missing Password Field Masking." https://cwe.mitre.org/data/definitions/549.html
- OWASP. "Authentication Cheat Sheet."
- W3C. "HTML5 input types - password."