Insufficient Psychological Acceptability
Description
Insufficient Psychological Acceptability occurs when security mechanisms are so difficult, confusing, or frustrating to use that users circumvent, disable, or work around them. Security controls must be usable and understandable to be effective. Overly complex password requirements, intrusive authentication, confusing permission dialogs, and poorly designed security UX lead users to find insecure workarounds.
Risk
Users create weak passwords that meet complex rules superficially. Security warnings become ignored due to warning fatigue. Users share credentials to avoid re-authentication. Security features disabled to improve workflow. Phishing attacks succeed because users are conditioned to click through warnings. Shadow IT emerges to avoid secure but unusable systems.
Solution
Design security that aligns with user workflows. Implement progressive security (stronger for sensitive actions). Use clear, actionable security messaging. Reduce friction for legitimate use while maintaining security. Conduct usability testing of security controls. Provide secure defaults that require minimal user intervention.
Common Consequences
| Impact | Details |
|---|---|
| Security | Scope: Circumvention Users disable or bypass security controls. |
| Authentication | Scope: Weak Credentials Complex rules lead to predictable passwords. |
| Training | Scope: Warning Fatigue Users ignore security warnings. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: Overly complex password requirements
public class UnusablePasswordValidator {
public ValidationResult validatePassword(String password) {
List<String> errors = new ArrayList<>();
// VULNERABLE: So many rules users write passwords down
if (password.length() < 16) {
errors.add("Password must be at least 16 characters");
}
if (password.length() > 20) {
errors.add("Password must be at most 20 characters");
}
if (!password.matches(".*[A-Z].*")) {
errors.add("Must contain uppercase letter");
}
if (!password.matches(".*[a-z].*")) {
errors.add("Must contain lowercase letter");
}
if (!password.matches(".*[0-9].*")) {
errors.add("Must contain number");
}
if (!password.matches(".*[!@#$%^&*()].*")) {
errors.add("Must contain special character from: !@#$%^&*()");
}
if (password.matches(".*(.)(\\1{2,}).*")) {
errors.add("No character can repeat more than twice");
}
if (password.matches(".*(012|123|234|345|456|567|678|789).*")) {
errors.add("No sequential numbers allowed");
}
if (password.matches(".*(abc|bcd|cde|def|efg).*")) {
errors.add("No sequential letters allowed");
}
// Must be changed every 30 days
// Cannot reuse last 24 passwords
// Result: Users create "Password1!" variations and write them down
return new ValidationResult(errors.isEmpty(), errors);
}
}
// VULNERABLE: Constant security interruptions
public class IntrusiveSecurityController {
@RequestMapping("/**")
public Object handleAllRequests(HttpServletRequest request) {
// VULNERABLE: Re-authenticate for every action
if (!authenticateUser(request)) {
return redirectToLogin(); // Users share passwords to avoid this
}
// VULNERABLE: CAPTCHA on every form
if (!verifyCaptcha(request)) {
return showCaptcha(); // Users find workarounds
}
// VULNERABLE: Security question for every sensitive page
if (isSensitivePage(request)) {
return askSecurityQuestion(); // Users pick easy questions
}
return processRequest(request);
}
}
// VULNERABLE: Confusing security warnings
public class ConfusingSecurityWarnings {
public void showCertificateWarning() {
// VULNERABLE: Technical jargon users don't understand
showDialog(
"SSL/TLS Certificate Error: " +
"The certificate chain validation failed. " +
"ERR_CERT_AUTHORITY_INVALID (0x800B0109). " +
"The certificate is not signed by a trusted authority. " +
"SHA-256 fingerprint: a4:b2:c1:d3:e5...",
"Proceed Anyway", "Cancel"
);
// Users learn to click "Proceed Anyway" for everything
}
}
# VULNERABLE: Password policy that leads to insecure behavior
class UnusablePasswordPolicy:
def validate(self, password):
errors = []
# VULNERABLE: Contradictory/impossible rules
if len(password) < 14:
errors.append("Minimum 14 characters")
if len(password) > 16:
errors.append("Maximum 16 characters")
# Requires complexity that forces predictable patterns
if not re.search(r'[A-Z]', password):
errors.append("Needs uppercase")
if not re.search(r'[a-z]', password):
errors.append("Needs lowercase")
if not re.search(r'[0-9]', password):
errors.append("Needs number")
if not re.search(r'[!@#$%^&*]', password):
errors.append("Needs special character")
# No common words - but users just add 1! to common words
common_words = ['password', 'company', 'admin', 'user']
for word in common_words:
if word.lower() in password.lower():
errors.append(f"Cannot contain '{word}'")
# VULNERABLE: 24-password history leads to Post-it notes
if password in self.get_password_history(user_id, count=24):
errors.append("Cannot reuse recent passwords")
return len(errors) == 0, errors
# VULNERABLE: Session timeout too aggressive
class IntrusiveSessionManager:
def __init__(self):
# VULNERABLE: 5 minute timeout causes constant re-login
self.session_timeout = 300 # seconds
def check_session(self, session):
if time.time() - session.last_activity > self.session_timeout:
# Users stay logged in on shared computers to avoid this
session.invalidate()
return False
return True
# VULNERABLE: Overly verbose security logging dialogs
def show_security_status():
# VULNERABLE: Information overload
message = """
Security Status Report:
- TLS 1.3 connection established
- Certificate: Valid (expires 2024-12-31)
- OCSP: Verified
- CT: 3 SCTs present
- HSTS: Enabled (max-age=31536000)
- CSP: strict-dynamic enforced
- X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin
...
[50 more technical details]
Do you want to continue?
"""
# Users learn to click through without reading
return show_dialog(message, ["Yes", "No"])
// VULNERABLE: Constant interruptions
class IntrusiveSecurityUI {
constructor() {
this.lastCaptchaTime = 0;
}
// VULNERABLE: CAPTCHA on every action
async performAction(action) {
// Users get frustrated and leave
await this.showCaptcha();
// Then security question
await this.askSecurityQuestion();
// Then MFA
await this.verifyMFA();
// Finally perform action
return action();
}
// VULNERABLE: Warning dialogs that train users to click through
showSecurityWarning() {
// Technical jargon and scary language
return showModal({
title: '⚠️ CRITICAL SECURITY WARNING ⚠️',
message: `
Your connection may be compromised!
Error Code: SEC_ERROR_UNKNOWN_ISSUER
The certificate is not trusted because no issuer chain was provided.
Technical Details: Certificate chain incomplete. OCSP response indicates...
[200 more words of technical content]
`,
buttons: ['I understand the risks, proceed anyway', 'Go back to safety']
});
// Users learn to always click "proceed"
}
// VULNERABLE: Password change forced too often
enforcePasswordChange(user) {
const daysSinceChange = (Date.now() - user.passwordChangedAt) / 86400000;
if (daysSinceChange > 30) {
// Users increment number: Password1! -> Password2! -> Password3!
this.forcePasswordChange();
}
}
}
// VULNERABLE: Confusing permission requests
function requestPermissions() {
// Users don't understand what they're agreeing to
return showPermissionDialog({
title: 'Permission Required',
message: 'This app needs the following permissions: ' +
'READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, ' +
'ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ' +
'READ_CONTACTS, WRITE_CONTACTS, CAMERA, MICROPHONE, ' +
'SYSTEM_ALERT_WINDOW, REQUEST_INSTALL_PACKAGES...',
buttons: ['Allow All', 'Deny']
});
// Users click "Allow All" to make it go away
}
Fixed Code
// SAFE: User-friendly password requirements
public class UsablePasswordValidator {
public ValidationResult validatePassword(String password) {
// SAFE: Focus on length over complexity
// Research shows length is more secure than complexity rules
if (password.length() < 12) {
return ValidationResult.fail(
"Password needs at least 12 characters. " +
"Tip: Try a memorable phrase like 'correct horse battery staple'"
);
}
// Check against breached passwords (not arbitrary rules)
if (isBreachedPassword(password)) {
return ValidationResult.fail(
"This password has appeared in data breaches. " +
"Please choose a different password for your security."
);
}
// Simple strength meter - not blocking rules
int strength = calculateStrength(password);
String strengthMessage = getStrengthFeedback(strength);
return ValidationResult.success(strengthMessage);
}
// SAFE: Clear, actionable feedback
private String getStrengthFeedback(int strength) {
if (strength >= 80) {
return "✓ Strong password";
} else if (strength >= 60) {
return "Good password - consider making it longer for extra security";
} else {
return "Acceptable - adding more words would make it stronger";
}
}
}
// SAFE: Progressive security
public class ProgressiveSecurityController {
@RequestMapping("/**")
public Object handleRequest(HttpServletRequest request) {
// Low-risk actions: minimal friction
if (isLowRiskAction(request)) {
return processRequest(request);
}
// Medium-risk: verify session is active
if (isMediumRiskAction(request)) {
if (!sessionService.isActive(request)) {
return redirectToLogin();
}
return processRequest(request);
}
// High-risk: step-up authentication
if (isHighRiskAction(request)) {
if (!hasRecentAuthentication(request, Duration.ofMinutes(5))) {
return requestReauthentication();
}
return processRequest(request);
}
return processRequest(request);
}
}
// SAFE: Clear, user-friendly security warnings
public class UserFriendlySecurityWarnings {
public void showCertificateWarning(CertificateError error) {
// SAFE: Plain language, clear actions
if (error.isSelfSigned()) {
showDialog(
"This website's security certificate wasn't issued by a " +
"trusted organization.\n\n" +
"This might mean:\n" +
"• You're on a test or internal site (okay)\n" +
"• Someone might be trying to intercept your data (not okay)\n\n" +
"If you're not sure, don't proceed.",
"Go back (recommended)",
"I understand, proceed anyway"
);
}
}
}
# SAFE: User-friendly password policy
class UsablePasswordPolicy:
def validate(self, password, username=None):
# SAFE: Simple length requirement
if len(password) < 12:
return False, "Please use at least 12 characters. Passphrases work great!"
# Check against known breached passwords
if self.is_breached(password):
return False, (
"This password appears in known data breaches. "
"Please choose a different one to stay secure."
)
# Prevent obvious patterns (not arbitrary complexity)
if username and username.lower() in password.lower():
return False, "Password shouldn't contain your username"
# SAFE: Helpful strength feedback instead of blocking
strength = self.estimate_strength(password)
if strength >= 3:
return True, "✓ Strong password"
elif strength >= 2:
return True, "Good password. Adding more words would make it even better."
else:
return True, "Acceptable password"
def is_breached(self, password):
# Check against HaveIBeenPwned or local breach database
password_hash = hashlib.sha1(password.encode()).hexdigest()
return self.breach_db.contains(password_hash[:5])
# SAFE: Reasonable session management
class UsableSessionManager:
def __init__(self):
# SAFE: Reasonable timeout with activity extension
self.idle_timeout = 3600 # 1 hour for normal pages
self.sensitive_timeout = 300 # 5 min for sensitive pages
self.absolute_timeout = 86400 # 24 hour maximum
def check_session(self, session, is_sensitive=False):
timeout = self.sensitive_timeout if is_sensitive else self.idle_timeout
idle_time = time.time() - session.last_activity
if idle_time > timeout:
# SAFE: Explain why and offer easy re-authentication
return {
'valid': False,
'message': f"For your security, please log in again. "
f"You've been idle for {int(idle_time/60)} minutes."
}
# Extend activity on action
session.last_activity = time.time()
return {'valid': True}
# SAFE: Progressive authentication
class ProgressiveAuth:
def get_required_auth_level(self, action):
# Different actions need different security levels
LOW_RISK = ['view_profile', 'browse', 'read']
MEDIUM_RISK = ['update_profile', 'change_settings']
HIGH_RISK = ['change_password', 'transfer_funds', 'delete_account']
if action in HIGH_RISK:
return 'reauthenticate' # Password + MFA
elif action in MEDIUM_RISK:
return 'session' # Valid session
else:
return 'none' # Public access
def verify_auth(self, user, action, credentials=None):
required = self.get_required_auth_level(action)
if required == 'none':
return True
if required == 'session':
return user.session.is_valid()
if required == 'reauthenticate':
# Only ask for re-auth for truly sensitive actions
return self.verify_credentials(user, credentials)
// SAFE: User-friendly security in JavaScript
class UserFriendlySecurity {
// SAFE: Clear, simple password guidance
validatePassword(password, email) {
// Length is key - NIST recommends 8+ min, we suggest 12+
if (password.length < 12) {
return {
valid: false,
message: 'Please use at least 12 characters. Try a phrase you can remember!',
suggestion: 'Example: "purple elephant jumps high"'
};
}
// Check breached passwords
if (this.isBreached(password)) {
return {
valid: false,
message: 'This password was found in a data breach. Please use a different one.'
};
}
// Helpful, not blocking
const strength = this.measureStrength(password);
return {
valid: true,
strength,
message: this.getStrengthMessage(strength)
};
}
getStrengthMessage(strength) {
const messages = {
'strong': '✓ Great password!',
'good': '✓ Good password',
'ok': '✓ Acceptable - longer would be better'
};
return messages[strength];
}
// SAFE: Plain-language security warnings
showSecurityWarning(type, details) {
const warnings = {
'certificate': {
title: 'Security Warning',
message: `
We can't verify this website's identity.
This could mean:
• The site is misconfigured
• Someone might be intercepting your connection
What should I do?
• If you expected to see this (like a company internal site), it may be okay
• If not, go back for your safety
`,
primaryAction: 'Go back',
secondaryAction: 'I understand, continue'
},
'unsaved': {
title: 'Unsaved Changes',
message: 'You have unsaved changes. Leave anyway?',
primaryAction: 'Stay',
secondaryAction: 'Leave without saving'
}
};
return showDialog(warnings[type]);
}
// SAFE: Progressive security based on risk
async performAction(action, context) {
const riskLevel = this.assessRisk(action, context);
if (riskLevel === 'low') {
return await action();
}
if (riskLevel === 'medium') {
// Just verify session is active
if (!this.isSessionActive()) {
await this.showQuickReauth();
}
return await action();
}
if (riskLevel === 'high') {
// Full re-authentication with clear explanation
const reauth = await this.showReauthDialog({
message: 'This action affects your account security. ' +
'Please verify your password.',
reason: action.description
});
if (reauth.success) {
return await action();
}
}
}
}
Exploited in the Wild
Password Patterns
Complex rules lead to "Password1!" patterns.
Warning Fatigue
Users trained to click through warnings fall for phishing.
Credential Sharing
Intrusive authentication leads to password sharing.
Tools to test/exploit
-
Usability testing frameworks.
-
Password pattern analysis.
-
User behavior analytics.
CVE Examples
-
Security bypasses due to user workarounds.
-
Phishing success from warning fatigue.
References
-
MITRE. "CWE-655: Insufficient Psychological Acceptability." https://cwe.mitre.org/data/definitions/655.html
-
NIST SP 800-63B. "Digital Identity Guidelines."