Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Comptoir
Comptoir-srv
Commits
56c30b69
Commit
56c30b69
authored
Mar 22, 2021
by
Fabrice Gangler
🎨
Browse files
FIX(anti-spam): reduce user creation by bots
parent
9dcd1254
Pipeline
#13448
passed with stage
in 4 minutes and 12 seconds
Changes
5
Pipelines
2
Hide whitespace changes
Inline
Side-by-side
src/Controller/Api/V1/UsersController.php
View file @
56c30b69
...
...
@@ -464,21 +464,153 @@ class UsersController extends AppController
}
/**
* Tooling for "add" method: create anti-spam tokens
* - create anti-spam tokens (get, post, form)
* - save tokens in user session
* - return token for HTTP GET method
*
* @return string token for HTTP GET method
*/
private
function
addUserCreateTokens
()
{
$prefixToken
=
bin2hex
(
openssl_random_pseudo_bytes
(
12
));
$getSuffixToken
=
bin2hex
(
openssl_random_pseudo_bytes
(
12
));
$postSuffixToken
=
bin2hex
(
openssl_random_pseudo_bytes
(
12
));
$formSuffixToken
=
bin2hex
(
openssl_random_pseudo_bytes
(
12
));
$dataToken
=
$this
->
request
->
session
()
->
read
(
'CreateUserAntiSpam'
);
if
(
is_null
(
$dataToken
))
{
$dataToken
=
[];
}
$dataToken
[
"
$prefixToken
"
]
=
[
"date"
=>
time
(),
"getToken"
=>
"
$prefixToken
-
$getSuffixToken
"
,
"postToken"
=>
"
$prefixToken
-
$postSuffixToken
"
,
"formToken"
=>
"
$prefixToken
-
$formSuffixToken
"
,
];
$this
->
request
->
session
()
->
write
(
'CreateUserAntiSpam'
,
$dataToken
);
return
"
$prefixToken
-
$getSuffixToken
"
;
}
/**
* Tooling for "add" method: get current anti-spam token based on current HTTP method
* @return string
*/
private
function
addUserGetCurrentToken
()
{
$method
=
strtolower
(
$this
->
request
->
method
());
if
(
$method
===
'get'
)
{
return
strip_tags
(
$this
->
request
->
query
(
't1'
));
}
elseif
(
$method
===
'post'
)
{
return
strip_tags
(
$this
->
request
->
query
(
't2'
));
}
return
''
;
// wrong HTTP method
}
/**
* Tooling for "add" method: check if current anti-spam token in URL is valid
* @return bool
*/
private
function
addUserIsValidTokenInUrl
()
{
$token
=
$this
->
addUserGetCurrentToken
();
if
(
empty
(
$token
))
{
return
false
;
// wrong HTTP method or token parameter in the URL does not exist
}
else
{
$method
=
strtolower
(
$this
->
request
->
method
());
$prefixToken
=
$this
->
addUserGetPrefixToken
(
$token
);
$availableTokens
=
$this
->
request
->
session
()
->
read
(
'CreateUserAntiSpam'
);
if
(
!
isset
(
$availableTokens
[
$prefixToken
]))
{
return
false
;
}
elseif
(
$availableTokens
[
$prefixToken
][
$method
.
'Token'
]
!==
$token
)
{
return
false
;
}
}
return
true
;
}
/**
* Tooling for "add" method: get anti-spam prefix token
*
* @param string $token
* @return mixed|string
*/
private
function
addUserGetPrefixToken
(
$token
)
{
$tokenData
=
explode
(
'-'
,
$token
);
return
$tokenData
[
0
];
}
/**
* Tooling for "add" method: get current token by type ('get', 'post' or 'form')
*
* @param string $type 'get', 'post' or 'form'
* @return string
*/
private
function
addUserGetTokenByType
(
string
$type
)
{
$prefixToken
=
$this
->
addUserGetPrefixToken
(
$this
->
addUserGetCurrentToken
());
$availableTokens
=
$this
->
request
->
session
()
->
read
(
'CreateUserAntiSpam'
);
$token
=
''
;
if
(
isset
(
$availableTokens
[
"
$prefixToken
"
][
$type
.
'Token'
]))
{
$token
=
$availableTokens
[
"
$prefixToken
"
][
$type
.
'Token'
];
}
return
$token
;
}
/**
* Tooling for "add" method: check if form is sent too quicky
* @return bool
*/
private
function
addUserIsFormSentTooQuickly
()
{
$prefixToken
=
$this
->
addUserGetPrefixToken
(
$this
->
addUserGetCurrentToken
());
$availableTokens
=
$this
->
request
->
session
()
->
read
(
'CreateUserAntiSpam'
);
if
(
isset
(
$availableTokens
[
"
$prefixToken
"
]))
{
$tokenTimestamp
=
$availableTokens
[
"
$prefixToken
"
][
'date'
];
$diffTimestamp
=
time
()
-
$tokenTimestamp
;
if
(
$diffTimestamp
>=
4
)
{
// @@@TODO magic number !!!
return
false
;
}
}
return
true
;
}
/**
* Add method
*
* @return void Redirects on successful add, renders view otherwise.
*/
public
function
add
()
{
$message
=
""
;
$lang
=
$this
->
selectedLanguage
;
if
(
!
empty
(
$this
->
request
->
data
))
{
$user
=
$this
->
Users
->
newEntity
(
$this
->
request
->
data
);
}
else
{
$user
=
$this
->
Users
->
newEntity
();
}
$message
=
""
;
// Force token-based URL (HTTP GET and POST methods), expect for json API
if
(
!
$this
->
request
->
is
(
'json'
)
&&
$this
->
addUserIsValidTokenInUrl
()
===
false
)
{
return
$this
->
redirect
(
"/
$lang
/users/add?t1="
.
$this
->
addUserCreateTokens
());
}
$dataToken
=
$this
->
request
->
session
()
->
read
(
'CreateUserAntiSpam'
);
if
(
$this
->
request
->
is
(
'post'
))
{
// Check that it's not a robot (expected token and speed submit), expect for json API
if
(
!
$this
->
request
->
is
(
'json'
))
{
$expectedFormToken
=
$this
->
addUserGetTokenByType
(
'form'
);
$formToken
=
$this
->
request
->
data
(
"formToken"
);
if
(
$expectedFormToken
!==
$formToken
|
$this
->
addUserIsFormSentTooQuickly
())
{
return
$this
->
redirect
(
"/
$lang
/users/add?t1="
.
$this
->
addUserCreateTokens
());
}
}
// do not allow to upload an avatar when creating an account
$this
->
request
->
data
[
'photo'
]
=
null
;
...
...
@@ -506,6 +638,7 @@ class UsersController extends AppController
if
(
$this
->
Users
->
save
(
$user
))
{
$message
=
"Success"
;
$this
->
Flash
->
success
(
__d
(
"Forms"
,
"Your are registred on the Comptoir du Libre, welcome !"
));
$this
->
request
->
session
()
->
write
(
'CreateUserAntiSpam'
,
[]);
// Unset tokens
if
(
!
$this
->
request
->
is
(
'json'
))
{
$currentUser
=
$this
->
Auth
->
identify
();
$currentUser
[
"user_type"
]
=
$this
->
Users
...
...
@@ -514,7 +647,6 @@ class UsersController extends AppController
$this
->
Auth
->
setUser
(
$currentUser
);
// REDIRECTS TO ---> /<lang>/users/<idUser>
$lang
=
$this
->
selectedLanguage
;
return
$this
->
redirect
(
"/
$lang
/users/"
.
$user
->
get
(
'id'
));
// REDIRECTS TO ---> /api/v1/users/view/<idUser>
// return $this->redirect(['action' => 'view', $user->get('id')]);
...
...
@@ -527,7 +659,16 @@ class UsersController extends AppController
}
$this
->
ValidationRules
->
config
(
'tableRegistry'
,
"Users"
);
$rules
=
$this
->
ValidationRules
->
get
();
$tokenForForm
=
$this
->
addUserGetTokenByType
(
'form'
);
$formUrl
=
"/
$lang
/users/add?t2="
.
$this
->
addUserGetTokenByType
(
'post'
);
$userTypes
=
$this
->
Users
->
UserTypes
->
find
(
'list'
,
[
'limit'
=>
200
]);
if
(
!
$this
->
request
->
is
(
'json'
))
{
$this
->
set
(
compact
([
'formUrl'
]));
$this
->
set
(
compact
([
'tokenForForm'
]));
}
$this
->
set
(
compact
(
'user'
,
'userTypes'
,
'rules'
,
'message'
));
$this
->
set
(
'_serialize'
,
[
'user'
,
'userTypes'
,
'rules'
,
'message'
,
'errors'
]);
...
...
src/Template/Api/V1/Users/add.ctp
View file @
56c30b69
...
...
@@ -14,7 +14,15 @@ $this->assign('title', __d("Forms", "Create an account {0}", $myMessage));
<div
class=
"row"
>
<div
class =
"col-xs-offset-3 col-xs-6"
>
<?=
$this
->
Form
->
create
(
$user
,
[
'type'
=>
'file'
,
"enctype"
=>
"multipart/form-data"
,
'id'
=>
"createAccountForm"
])
?>
<?=
$this
->
Form
->
create
(
$user
,
[
'url'
=>
"
$formUrl
"
,
'type'
=>
'file'
,
"enctype"
=>
"multipart/form-data"
,
'id'
=>
"createAccountForm"
])
?>
<?=
$this
->
Form
->
input
(
'formToken'
,
[
'value'
=>
$tokenForForm
,
'empty'
=>
false
,
"type"
=>
"hidden"
,
"required"
=>
"required"
,
"escape"
=>
false
,
]);
?>
<fieldset>
<legend>
<?=
"<h1>"
.
__d
(
"Forms"
,
'Create an account'
)
.
"</h1>"
?>
</legend>
<div
class =
"form-group"
>
...
...
src/TestSuite/Codeception/AcceptanceTester.php
View file @
56c30b69
...
...
@@ -122,11 +122,15 @@ class AcceptanceTester extends Actor
$I
=
$this
;
$I
->
amOnPage
(
'/'
);
$I
->
click
(
'//nav/ul[2]/li[2]/a'
);
$I
->
seeInCurrentUrl
(
'/users/add'
);
$I
->
seeInCurrentUrl
(
'/users/add
?t1=
'
);
$random
=
$I
->
getCurrentRandom
();
$randomUsername
=
"
$username
"
.
"_
$random
"
;
$randomEmail
=
str_replace
(
"@"
,
"
$random
@"
,
"
$email
"
);
// echo "\n-------------\n$randomUsername\n$randomEmail\n---------\n";
// Add delay to look like a real user and not a robot
sleep
(
4
);
// @@@TODO magic number !!!
$I
->
submitForm
(
'#createAccountForm'
,
[
...
...
tests/TestCase/Controller/Api/V1/UsersControllerTest.php
View file @
56c30b69
...
...
@@ -175,6 +175,182 @@ class UsersControllerTest extends ApiIntegrationTestCase
];
}
/**
* Test add method like a end user (HTML form)
* - Check that account creation URL redirects to a URL with a "t1" parameter containing a token
* - Check that form contains the tokens for the form processing URL and the hidden field
* - Try to display form with a wrong token in URL
* - Try to display form without a token in URL
* - Try to send form with a wrong token in URL
* - Try to send form without a token in URL
* - Try to send form with a valid token in URL, but without a token input in form
* - Try to send form with a valid token in URL, but with a wrong token input in form
* - Try to send form with valid tokens in URL and form, but too quickly (as a bot)
* - Send form with valid tokens in URL and form, like a real user (not too quickly)
*
* URL: /en/users/add ---> redirect to /en/users/add?t1=<token>
* /fr/users/add ---> redirect to /en/users/add?t1=<token>
* /fr/users/add?t1=<token> GET method
* /fr/users/add?t2=<token> POST method
*
* @group public
* @group user
* @group createUser
*
* @return void
*/
public
function
testAddHtmlForm
()
{
// French user:
$testeddUrl
=
'/fr/users/add'
;
$expectedUrlPrefix
=
'/fr/users/add?t1='
;
$title
=
"<title>Créer un compte"
;
// Form data
$emailFixture1
=
'bob_'
.
mt_rand
()
.
'@example.com'
;
$userFixture1
=
[
'username'
=>
'Bob'
,
'url'
=>
'url'
,
'user_type_id'
=>
1
,
'description'
=>
'A description'
,
'role'
=>
'user'
,
'password'
=>
'passwd'
,
'confirm_password'
=>
'passwd'
,
'email'
=>
$emailFixture1
,
];
// Check that the following users do not exist in the database before creating it.
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// Anonymous user
$this
->
setAnonymousUserSession
();
$this
->
get
(
'/'
);
$this
->
assertArrayNotHasKey
(
'CreateUserAntiSpam'
,
$_SESSION
);
// Check that the account creation URL
// redirects to a URL with a "t1" parameter containing a token.
$this
->
get
(
$testeddUrl
);
$this
->
assertResponseCode
(
302
);
$this
->
assertRedirectContains
(
$expectedUrlPrefix
);
$this
->
assertArrayHasKey
(
'CreateUserAntiSpam'
,
$_SESSION
);
$this
->
assertEquals
(
1
,
\
count
(
$_SESSION
[
'CreateUserAntiSpam'
]));
$tokenPrefix1
=
array_key_first
(
$_SESSION
[
'CreateUserAntiSpam'
]);
$tokenGetMethod1
=
$_SESSION
[
'CreateUserAntiSpam'
][
$tokenPrefix1
][
'getToken'
];
$tokenPostMethod1
=
$_SESSION
[
'CreateUserAntiSpam'
][
$tokenPrefix1
][
'postToken'
];
$tokenForm1
=
$_SESSION
[
'CreateUserAntiSpam'
][
$tokenPrefix1
][
'formToken'
];
$expectedUrl1
=
$expectedUrlPrefix
.
$tokenGetMethod1
;
$headers
=
$this
->
_response
->
header
();
$this
->
assertEquals
(
$expectedUrl1
,
$headers
[
'Location'
]);
// Save session data for future requests
$backupSession
=
$_SESSION
;
// Add form token to fixtures
$userFixture1
[
'formToken'
]
=
$tokenForm1
;
// Try to display form with a wrong token in the URL
// --> redirect to empty form (with new generated token)
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
get
(
$expectedUrlPrefix
.
"bad45-token45"
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
// Try to display form without a token in the URL
// --> redirect to empty form (with new generated token)
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
get
(
$testeddUrl
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
// Check that form contains the tokens
// for the form processing URL and the hidden field
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$r
=
$this
->
checkUrlOk
(
$expectedUrl1
,
[
'html'
]);
$html
=
$r
[
'html'
][
'data'
];
$this
->
assertContains
(
'<html lang="fr">'
,
$html
);
$this
->
assertContains
(
"
$title
"
,
$html
);
$this
->
assertContains
(
"action=
\"
/fr/users/add?t2=
$tokenPostMethod1
\"
"
,
$html
);
$this
->
assertContains
(
"id=
\"
formtoken
\"
value=
\"
$tokenForm1
\"
"
,
$html
);
// Try to send form with a wrong token in the URL
// --> redirect to empty form (with new generated token)
// --> new user has not been created
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
$this
->
post
(
"/fr/users/add?t2=
$tokenPostMethod1
"
.
"badTokenSuffix"
,
$userFixture1
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
// Try to send form without a token in the URL
// --> redirect to empty form (with new generated token)
// --> new user has not been created
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
$this
->
post
(
"/fr/users/add"
,
$userFixture1
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
// Try to send form with a valid token in the URL, but without a token input in form
// --> redirect to empty form (with new generated token)
// --> new user has not been created
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
$userFixture1WithoutFormToken
=
$userFixture1
;
unset
(
$userFixture1WithoutFormToken
[
'formToken'
]);
$this
->
post
(
"/fr/users/add?t2=
$tokenPostMethod1
"
,
$userFixture1WithoutFormToken
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
// Try to send form with a valid token in the URL, but with a wrong token input in form
// --> redirect to empty form (with new generated token)
// --> new user has not been created
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
$userFixture1WithBadFormToken
=
$userFixture1
;
$userFixture1WithBadFormToken
[
'formToken'
]
=
'bad45-token45'
;
$this
->
post
(
"/fr/users/add?t2=
$tokenPostMethod1
"
,
$userFixture1WithBadFormToken
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
// Try to send form with a valid token in the URL, but too quickly (as a bot)
// --> redirect to empty form (with new generated token)
// --> new user has not been created
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
$this
->
post
(
"/fr/users/add?t2=
$tokenPostMethod1
"
,
$userFixture1
);
$this
->
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
);
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
// Add delay to look like a real user and not a robot
sleep
(
4
);
// @@@TODO magic number !!!
// Send form with a valid token in the URL, like a real user (not too quickly)
// --> new user has been created
$this
->
session
(
$backupSession
);
// Use backuped SESSION
$this
->
assertEquals
(
0
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// not exist in DB
$this
->
post
(
"/fr/users/add?t2=
$tokenPostMethod1
"
,
$userFixture1
);
$this
->
assertResponseCode
(
302
);
$headers
=
$this
->
_response
->
header
();
$this
->
assertEquals
(
"/fr/users/11"
,
$headers
[
'Location'
]);
$this
->
assertEquals
(
1
,
$this
->
Users
->
find
()
->
where
([
'email'
=>
$emailFixture1
])
->
count
());
// exist in DB
}
/**
* Common checks for add method like a end user (HTML form)
* -> redirect to empty form (with new generated token)
*
* @param $expectedUrlPrefix
*/
private
function
commonCheckForAddHtmlForm
(
$expectedUrlPrefix
)
{
$this
->
assertResponseCode
(
302
);
$this
->
assertRedirectContains
(
$expectedUrlPrefix
);
$this
->
assertArrayHasKey
(
'CreateUserAntiSpam'
,
$_SESSION
);
$this
->
assertEquals
(
2
,
\
count
(
$_SESSION
[
'CreateUserAntiSpam'
]));
$tokenPrefix
=
array_key_last
(
$_SESSION
[
'CreateUserAntiSpam'
]);
$tokenGetMethod
=
$_SESSION
[
'CreateUserAntiSpam'
][
$tokenPrefix
][
'getToken'
];
$expectedUrl
=
$expectedUrlPrefix
.
$tokenGetMethod
;
$headers
=
$this
->
_response
->
header
();
$this
->
assertEquals
(
$expectedUrl
,
$headers
[
'Location'
]);
}
/**
* Test add method
...
...
webroot/robots.txt
View file @
56c30b69
...
...
@@ -10,6 +10,8 @@ Disallow:
# Blocking all web crawlers for specific content
User-agent: *
Disallow: /api/
Disallow: /fr/users/add
Disallow: /en/users/add
# Allowing all web crawlers access to all content
User-agent: *
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment