Aug 26 - Enhance security 4
CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Pytest (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled

This commit is contained in:
2026-08-26 14:19:25 -04:00
parent cc216b0d98
commit b84a6d9245
11 changed files with 546 additions and 33 deletions
+41 -3
View File
@@ -131,7 +131,7 @@ def _apply_reencrypted_items(user_id: int, items, allow_partial: bool = False) -
@auth_bp.route('/register', methods=['POST'])
@limiter.limit('10 per minute')
@limiter.limit('5 per minute')
def register():
data = request.get_json(silent=True) or {}
email = (data.get('email') or '').strip().lower()
@@ -147,8 +147,46 @@ def register():
if not enc_key_salt:
return jsonify({'error': 'enc_key_salt is required'}), 400
# ── Account-existence must not be observable ────────────────────────────
#
# This used to answer 409 "Email already registered", which let anyone probe
# whether a given address has a PassKeeper account — a useful target list for
# phishing, and exactly the kind of thing a password manager should not leak.
#
# Both branches now return the identical 202 body. The wording sends the user
# to the sign-in page either way, which is the correct next step in both
# cases: registering an address that already exists is harmless because the
# user simply signs in with the password they already have.
#
# Timing has to match too. Creating an account runs Argon2id (deliberately
# slow); returning early without it would make "exists" measurably faster and
# reinstate the oracle through the side door. So the existing-account branch
# performs and discards an equivalent hash.
#
# NOTE: fully closing this needs email verification (roadmap item 4) so the
# address owner is told when someone tries to register it. Until then this
# removes the oracle but cannot notify the legitimate owner.
generic_response = jsonify({
'message': (
'If that email address was available, your account has been created. '
'Please sign in.'
)
}), 202
time.sleep(0.1) # flatten timing across both branches
if User.query.filter_by(email=email).first():
return jsonify({'error': 'Email already registered'}), 409
hash_auth_token(auth_hash) # equalise work; result intentionally discarded
AuditLog.log(
user_id=0, # no account to attribute this to
action='auth.register_duplicate',
resource_type='user',
resource_id=None,
detail='Registration attempted for an address that already exists',
ip_address=client_ip(),
)
db.session.commit()
return generic_response
master_hash = hash_auth_token(auth_hash)
user = User(email=email, master_hash=master_hash, enc_key_salt=enc_key_salt)
@@ -165,7 +203,7 @@ def register():
)
db.session.commit()
return jsonify({'message': 'Account created successfully'}), 201
return generic_response
@auth_bp.route('/login', methods=['POST'])