mirror of
https://github.com/penpot/penpot.git
synced 2026-08-07 21:38:48 +00:00
🐛 Add backend password validation with complexity rules and dictionary check
Enforce minimum 8-character password length, require at least 1 lowercase letter, 1 uppercase letter, 1 digit, and 1 special character, and reject common passwords using Passay library with bundled wordlist during registration and password change flows. AI-assisted-by: qwen3.7-plus
This commit is contained in:
parent
41afcba297
commit
5f0175a641
@ -48,6 +48,7 @@
|
||||
|
||||
buddy/buddy-hashers {:mvn/version "2.0.167"}
|
||||
buddy/buddy-sign {:mvn/version "3.6.1-359"}
|
||||
org.passay/passay {:mvn/version "1.6.6"}
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
|
||||
|
||||
|
||||
223
backend/resources/app/common-passwords.txt
Normal file
223
backend/resources/app/common-passwords.txt
Normal file
@ -0,0 +1,223 @@
|
||||
password
|
||||
123456
|
||||
12345678
|
||||
1234
|
||||
qwerty
|
||||
12345
|
||||
dragon
|
||||
pussy
|
||||
baseball
|
||||
football
|
||||
letmein
|
||||
monkey
|
||||
696969
|
||||
abc123
|
||||
mustang
|
||||
michael
|
||||
shadow
|
||||
master
|
||||
jennifer
|
||||
111111
|
||||
2000
|
||||
jordan
|
||||
superman
|
||||
harley
|
||||
1234567
|
||||
fuckme
|
||||
hunter
|
||||
fuckyou
|
||||
trustno1
|
||||
ranger
|
||||
buster
|
||||
thomas
|
||||
tigger
|
||||
robert
|
||||
soccer
|
||||
fuck
|
||||
batman
|
||||
test
|
||||
pass
|
||||
killer
|
||||
hockey
|
||||
george
|
||||
charlie
|
||||
andrew
|
||||
michelle
|
||||
love
|
||||
sunshine
|
||||
jessica
|
||||
asshole
|
||||
6969
|
||||
pepper
|
||||
daniel
|
||||
access
|
||||
123456789
|
||||
654321
|
||||
joshua
|
||||
maggie
|
||||
starwars
|
||||
silver
|
||||
william
|
||||
dallas
|
||||
yankees
|
||||
123123
|
||||
ashley
|
||||
666666
|
||||
hello
|
||||
amanda
|
||||
orange
|
||||
biteme
|
||||
freedom
|
||||
computer
|
||||
sexy
|
||||
thunder
|
||||
nicole
|
||||
ginger
|
||||
heather
|
||||
hammer
|
||||
summer
|
||||
corvette
|
||||
taylor
|
||||
fucker
|
||||
austin
|
||||
1111
|
||||
merlin
|
||||
matthew
|
||||
121212
|
||||
golfer
|
||||
cheese
|
||||
princess
|
||||
martin
|
||||
chelsea
|
||||
patrick
|
||||
richard
|
||||
diamond
|
||||
yellow
|
||||
bigdog
|
||||
secret
|
||||
asdfgh
|
||||
sparky
|
||||
cowboy
|
||||
camaro
|
||||
anthony
|
||||
matrix
|
||||
falcon
|
||||
iloveyou
|
||||
bailey
|
||||
guitar
|
||||
jackson
|
||||
purple
|
||||
scooter
|
||||
phoenix
|
||||
aaaaaa
|
||||
morgan
|
||||
tigers
|
||||
porsche
|
||||
mickey
|
||||
maverick
|
||||
cookie
|
||||
nascar
|
||||
peanut
|
||||
justin
|
||||
131313
|
||||
money
|
||||
horny
|
||||
samantha
|
||||
panties
|
||||
steelers
|
||||
joseph
|
||||
snoopy
|
||||
boomer
|
||||
whatever
|
||||
iceman
|
||||
smokey
|
||||
gateway
|
||||
dakota
|
||||
cowboys
|
||||
eagles
|
||||
chicken
|
||||
dick
|
||||
black
|
||||
zxcvbn
|
||||
please
|
||||
andrea
|
||||
ferrari
|
||||
knight
|
||||
hardcore
|
||||
compaq
|
||||
coffee
|
||||
booboo
|
||||
bitch
|
||||
bulldog
|
||||
xxxxxx
|
||||
welcome
|
||||
james
|
||||
player
|
||||
ncc1701
|
||||
wizard
|
||||
sbpnb
|
||||
december
|
||||
hello123
|
||||
admin
|
||||
qwerty123
|
||||
1q2w3e4r
|
||||
1q2w3e4r5t
|
||||
password1
|
||||
password123
|
||||
iloveu
|
||||
letmein1
|
||||
abc1234
|
||||
qwertyuiop
|
||||
asdfghjkl
|
||||
zxcvbnm
|
||||
1234567890
|
||||
0987654321
|
||||
11223344
|
||||
admin123
|
||||
root
|
||||
toor
|
||||
pass123
|
||||
test123
|
||||
guest
|
||||
default
|
||||
changeme
|
||||
temp
|
||||
temp123
|
||||
passwd
|
||||
passw0rd
|
||||
p@ssword
|
||||
p@ssw0rd
|
||||
qwerty1
|
||||
abc12345
|
||||
123321
|
||||
12345678910
|
||||
asdf
|
||||
asdfasdf
|
||||
qwer
|
||||
qwerqwer
|
||||
zxcv
|
||||
zxcvzxcv
|
||||
!@#$%^&*
|
||||
!@#$%
|
||||
qazwsx
|
||||
qazwsxedc
|
||||
1qaz2wsx
|
||||
1qazxsw2
|
||||
pass1234
|
||||
test1234
|
||||
admin1234
|
||||
letmein123
|
||||
welcome1
|
||||
welcome123
|
||||
monkey123
|
||||
dragon123
|
||||
master123
|
||||
shadow123
|
||||
sunshine1
|
||||
princess1
|
||||
football1
|
||||
baseball1
|
||||
soccer1
|
||||
hockey1
|
||||
batman1
|
||||
superman1
|
||||
62
backend/src/app/auth/passwords.clj
Normal file
62
backend/src/app/auth/passwords.clj
Normal file
@ -0,0 +1,62 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.auth.passwords
|
||||
"Password strength validation using Passay library."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[clojure.java.io :as io])
|
||||
(:import
|
||||
[org.passay CharacterCharacteristicsRule CharacterRule DictionaryRule EnglishCharacterData PasswordData]
|
||||
[org.passay.dictionary ArrayWordList WordListDictionary]))
|
||||
|
||||
(defonce ^:private dictionary
|
||||
(let [lines (line-seq (io/reader (io/resource "app/common-passwords.txt")))
|
||||
words (into-array String (sort lines))
|
||||
word-list (ArrayWordList. words)]
|
||||
(WordListDictionary. word-list)))
|
||||
|
||||
(defonce ^:private dictionary-rule
|
||||
(DictionaryRule. dictionary))
|
||||
|
||||
(defonce ^:private character-characteristics-rule
|
||||
(doto (CharacterCharacteristicsRule.)
|
||||
(.setRules [(CharacterRule. EnglishCharacterData/LowerCase 1)
|
||||
(CharacterRule. EnglishCharacterData/UpperCase 1)
|
||||
(CharacterRule. EnglishCharacterData/Digit 1)
|
||||
(CharacterRule. EnglishCharacterData/Special 1)])
|
||||
(.setNumberOfCharacteristics 4)))
|
||||
|
||||
(defn validate-password
|
||||
"Validates password strength.
|
||||
Returns nil if valid, or raises exception if invalid.
|
||||
Checks:
|
||||
- Minimum length of 8 characters
|
||||
- At least 1 lowercase letter
|
||||
- At least 1 uppercase letter
|
||||
- At least 1 digit
|
||||
- At least 1 special character
|
||||
- Not in common password dictionary"
|
||||
[password]
|
||||
(when (< (count password) 8)
|
||||
(ex/raise :type :validation
|
||||
:code :weak-password
|
||||
:hint "password must be at least 8 characters"))
|
||||
|
||||
(let [password-data (PasswordData. password)]
|
||||
(let [char-result (.validate character-characteristics-rule password-data)]
|
||||
(when-not (.isValid char-result)
|
||||
(ex/raise :type :validation
|
||||
:code :weak-password
|
||||
:hint "password must contain at least 1 lowercase letter, 1 uppercase letter, 1 digit, and 1 special character"
|
||||
:details (mapv #(.getErrorCode %) (.getDetails char-result)))))
|
||||
|
||||
(let [dict-result (.validate dictionary-rule password-data)]
|
||||
(when-not (.isValid dict-result)
|
||||
(ex/raise :type :validation
|
||||
:code :weak-password
|
||||
:hint "password is too common"
|
||||
:details (mapv #(.getErrorCode %) (.getDetails dict-result)))))))
|
||||
@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.auth.oidc :as oidc]
|
||||
[app.auth.passwords :as passwords]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
@ -240,6 +241,9 @@
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
;; Validate password strength against common password dictionary
|
||||
(passwords/validate-password (:password params))
|
||||
|
||||
(when (eml/has-bounce-reports? cfg (:email params))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-permanent-bounces
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
(ns app.rpc.commands.profile
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.auth.passwords :as passwords]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
@ -200,6 +201,9 @@
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
;; Validate password strength against common password dictionary
|
||||
(passwords/validate-password (:password params))
|
||||
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
|
||||
(->> (rph/get-request params)
|
||||
|
||||
@ -189,7 +189,7 @@
|
||||
(let [params (merge {:id (mk-uuid "profile" i)
|
||||
:fullname (str "Profile " i)
|
||||
:email (str "profile" i ".test@nodomain.com")
|
||||
:password "123123"
|
||||
:password "Test123!"
|
||||
:is-demo false}
|
||||
params)]
|
||||
(db/run! system
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :login-with-password
|
||||
:email "profile1.test@nodomain.com"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
|
||||
#_(th/print-result! out)
|
||||
@ -56,7 +56,7 @@
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :login-with-password
|
||||
:email "profile1.test@nodomain.com"
|
||||
:password "123123"}
|
||||
:password "Test123!"}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(let [error (:error out)]
|
||||
@ -69,7 +69,7 @@
|
||||
(let [profile (th/create-profile* 1 {:is-active true})
|
||||
data {::th/type :login-with-password
|
||||
:email "profile1.test@nodomain.com"
|
||||
:password "123123"}
|
||||
:password "Test123!"}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
@ -403,7 +403,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"
|
||||
:password "Foobar12!"
|
||||
:utm_campaign "utma"
|
||||
:mtm_campaign "mtma"}
|
||||
out (th/command! data)
|
||||
@ -444,7 +444,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
@ -463,7 +463,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
@ -498,7 +498,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
@ -521,7 +521,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
@ -547,7 +547,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
@ -576,7 +576,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
@ -614,7 +614,7 @@
|
||||
:invitation-token itoken
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
|
||||
{prep-result :result prep-error :error} (th/command! prep-data)]
|
||||
(t/is (nil? prep-error))
|
||||
@ -659,7 +659,7 @@
|
||||
:invitation-token itoken
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
|
||||
{prep-result :result prep-error :error} (th/command! prep-data)]
|
||||
(t/is (nil? prep-error))
|
||||
@ -692,7 +692,7 @@
|
||||
:invitation-token itoken
|
||||
:email "user@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
@ -712,7 +712,7 @@
|
||||
:invitation-token itoken
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
@ -733,7 +733,7 @@
|
||||
:invitation-token itoken
|
||||
:email "user@example.com"
|
||||
:fullname "foobar"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
@ -754,7 +754,7 @@
|
||||
:invitation-token itoken
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
@ -767,7 +767,7 @@
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
@ -780,7 +780,7 @@
|
||||
data {::th/type :prepare-register-profile
|
||||
:fullname "foobar"
|
||||
:email (:email profile)
|
||||
:password "foobar"}
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
@ -793,7 +793,7 @@
|
||||
data {::th/type :prepare-register-profile
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}]
|
||||
:password "Foobar12!"}]
|
||||
|
||||
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||
|
||||
@ -808,7 +808,7 @@
|
||||
data {::th/type :prepare-register-profile
|
||||
:fullname "foobar"
|
||||
:email "user@example.com"
|
||||
:password "foobar"}]
|
||||
:password "Foobar12!"}]
|
||||
|
||||
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
||||
|
||||
@ -1131,8 +1131,8 @@
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :update-profile-password
|
||||
::rpc/profile-id (:id profile)
|
||||
:old-password "123123"
|
||||
:password "foobarfoobar"}
|
||||
:old-password "Test123!"
|
||||
:password "Foobar12!"}
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))))
|
||||
@ -1143,7 +1143,7 @@
|
||||
data {::th/type :update-profile-password
|
||||
::rpc/profile-id (:id profile)
|
||||
:old-password "badpassword"
|
||||
:password "foobarfoobar"}
|
||||
:password "Foobar12!"}
|
||||
{:keys [result error] :as out} (th/command! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
@ -1154,7 +1154,7 @@
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :update-profile-password
|
||||
::rpc/profile-id (:id profile)
|
||||
:old-password "123123"
|
||||
:old-password "Test123!"
|
||||
:password "profile1.test@nodomain.com"}
|
||||
{:keys [result error] :as out} (th/command! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
@ -1202,3 +1202,49 @@
|
||||
(t/is (true? (get-in props [:props :onboarding-viewed])))
|
||||
(t/is (false? (get-in props [:props :newsletter-updates])))
|
||||
(t/is (= :wasm (get-in props [:props :renderer]))))))
|
||||
|
||||
|
||||
(t/deftest prepare-register-profile-password-too-short
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:fullname "foobar"
|
||||
:password "123"}
|
||||
out (th/command! data)]
|
||||
(t/is (th/ex-info? (:error out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :weak-password))))
|
||||
|
||||
|
||||
(t/deftest prepare-register-profile-weak-password
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:fullname "foobar"
|
||||
:password "password123"}
|
||||
out (th/command! data)]
|
||||
(t/is (th/ex-info? (:error out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :weak-password))))
|
||||
|
||||
|
||||
(t/deftest update-profile-password-too-short
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :update-profile-password
|
||||
::rpc/profile-id (:id profile)
|
||||
:old-password "Test123!"
|
||||
:password "123"}
|
||||
out (th/command! data)]
|
||||
(t/is (th/ex-info? (:error out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :weak-password))))
|
||||
|
||||
|
||||
(t/deftest update-profile-password-weak-password
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :update-profile-password
|
||||
::rpc/profile-id (:id profile)
|
||||
:old-password "Test123!"
|
||||
:password "qwerty"}
|
||||
out (th/command! data)]
|
||||
(t/is (th/ex-info? (:error out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :weak-password))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user