Fossil SCM

Add the ability to enable users to request an email message that contains a special secure hyperlink that they can follow to reset their password.

drh 2023-01-07 15:36 trunk merge
Commit 07bfe3fee3a94d4ccc4a1a628d26c34d814edb940988a706091d71a25b77bf2c
+1
--- src/blob.c
+++ src/blob.c
@@ -124,10 +124,11 @@
124124
** Other replacements for ctype.h functions.
125125
*/
126126
int fossil_islower(char c){ return c>='a' && c<='z'; }
127127
int fossil_isupper(char c){ return c>='A' && c<='Z'; }
128128
int fossil_isdigit(char c){ return c>='0' && c<='9'; }
129
+int fossil_isxdigit(char c){ return (c>='0' && c<='9') || (c>='a' && c<='f'); }
129130
int fossil_tolower(char c){
130131
return fossil_isupper(c) ? c - 'A' + 'a' : c;
131132
}
132133
int fossil_toupper(char c){
133134
return fossil_islower(c) ? c - 'a' + 'A' : c;
134135
--- src/blob.c
+++ src/blob.c
@@ -124,10 +124,11 @@
124 ** Other replacements for ctype.h functions.
125 */
126 int fossil_islower(char c){ return c>='a' && c<='z'; }
127 int fossil_isupper(char c){ return c>='A' && c<='Z'; }
128 int fossil_isdigit(char c){ return c>='0' && c<='9'; }
 
129 int fossil_tolower(char c){
130 return fossil_isupper(c) ? c - 'A' + 'a' : c;
131 }
132 int fossil_toupper(char c){
133 return fossil_islower(c) ? c - 'a' + 'A' : c;
134
--- src/blob.c
+++ src/blob.c
@@ -124,10 +124,11 @@
124 ** Other replacements for ctype.h functions.
125 */
126 int fossil_islower(char c){ return c>='a' && c<='z'; }
127 int fossil_isupper(char c){ return c>='A' && c<='Z'; }
128 int fossil_isdigit(char c){ return c>='0' && c<='9'; }
129 int fossil_isxdigit(char c){ return (c>='0' && c<='9') || (c>='a' && c<='f'); }
130 int fossil_tolower(char c){
131 return fossil_isupper(c) ? c - 'A' + 'a' : c;
132 }
133 int fossil_toupper(char c){
134 return fossil_islower(c) ? c - 'a' + 'A' : c;
135
+7
--- src/db.c
+++ src/db.c
@@ -4499,10 +4499,17 @@
44994499
** displayed using unadorned HTML ("skinless").
45004500
**
45014501
** If repolist-skin has a value of 2, then the repository is omitted from
45024502
** the list in use cases 1 through 4, but not for 5 and 6.
45034503
*/
4504
+/*
4505
+** SETTING: self-pw-reset boolean default=off sensitive
4506
+** Allow users to request that an email containing a hyperlink
4507
+** to the /resetpw page be sent to their email address of record,
4508
+** thus allowing forgetful users to reset their forgotten passwords
4509
+** without administrator involvement.
4510
+*/
45044511
/*
45054512
** SETTING: self-register boolean default=off sensitive
45064513
** Allow users to register themselves through the HTTP UI.
45074514
** This is useful if you want to see other names than
45084515
** "Anonymous" in e.g. ticketing system. On the other hand
45094516
--- src/db.c
+++ src/db.c
@@ -4499,10 +4499,17 @@
4499 ** displayed using unadorned HTML ("skinless").
4500 **
4501 ** If repolist-skin has a value of 2, then the repository is omitted from
4502 ** the list in use cases 1 through 4, but not for 5 and 6.
4503 */
 
 
 
 
 
 
 
4504 /*
4505 ** SETTING: self-register boolean default=off sensitive
4506 ** Allow users to register themselves through the HTTP UI.
4507 ** This is useful if you want to see other names than
4508 ** "Anonymous" in e.g. ticketing system. On the other hand
4509
--- src/db.c
+++ src/db.c
@@ -4499,10 +4499,17 @@
4499 ** displayed using unadorned HTML ("skinless").
4500 **
4501 ** If repolist-skin has a value of 2, then the repository is omitted from
4502 ** the list in use cases 1 through 4, but not for 5 and 6.
4503 */
4504 /*
4505 ** SETTING: self-pw-reset boolean default=off sensitive
4506 ** Allow users to request that an email containing a hyperlink
4507 ** to the /resetpw page be sent to their email address of record,
4508 ** thus allowing forgetful users to reset their forgotten passwords
4509 ** without administrator involvement.
4510 */
4511 /*
4512 ** SETTING: self-register boolean default=off sensitive
4513 ** Allow users to register themselves through the HTTP UI.
4514 ** This is useful if you want to see other names than
4515 ** "Anonymous" in e.g. ticketing system. On the other hand
4516
+500 -15
--- src/login.c
+++ src/login.c
@@ -510,10 +510,19 @@
510510
zPattern = mprintf("%s/login*", g.zBaseURL);
511511
rc = sqlite3_strglob(zPattern, zReferer)==0;
512512
fossil_free(zPattern);
513513
return rc;
514514
}
515
+
516
+/*
517
+** Return true if users are allowed to reset their own passwords.
518
+*/
519
+int login_self_password_reset_available(void){
520
+ if( !db_get_boolean("self-pw-reset",0) ) return 0;
521
+ if( !alert_tables_exist() ) return 0;
522
+ return 1;
523
+}
515524
516525
/*
517526
** Return TRUE if self-registration is available. If the zNeeded
518527
** argument is not NULL, then only return true if self-registration is
519528
** available and any of the capabilities named in zNeeded are available
@@ -560,10 +569,16 @@
560569
const int noAnon = P("noanon")!=0;
561570
int rememberMe; /* If true, use persistent cookie, else
562571
session cookie. Toggled per
563572
checkbox. */
564573
574
+ if( P("pwreset")!=0 && login_self_password_reset_available() ){
575
+ /* If the "Reset Password" button in the form was pressed, render
576
+ ** the Request Password Reset page in place of this one. */
577
+ login_reqpwreset_page();
578
+ return;
579
+ }
565580
login_check_credentials();
566581
fossil_redirect_to_https_if_needed(1);
567582
sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
568583
constant_time_cmp_function, 0, 0);
569584
zUsername = P("u");
@@ -613,10 +628,14 @@
613628
char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
614629
char *zChngPw;
615630
char *zErr;
616631
int rc;
617632
633
+ /* vvvvvvv--- tag-20230106-1 ----vvvvvv
634
+ **
635
+ ** Replicate changes made below to tag-20230106-2
636
+ */
618637
db_unprotect(PROTECT_USER);
619638
db_multi_exec(
620639
"UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
621640
);
622641
zChngPw = mprintf(
@@ -627,10 +646,16 @@
627646
zNew1, g.zLogin, g.zLogin
628647
);
629648
fossil_free(zNewPw);
630649
rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
631650
db_protect_pop();
651
+ /*
652
+ ** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^
653
+ **
654
+ ** Replicate changes above to tag-20230106-2
655
+ */
656
+
632657
if( rc ){
633658
zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
634659
fossil_free(zErr);
635660
}else{
636661
redirect_to_g();
@@ -770,10 +795,16 @@
770795
if( !noAnon && login_self_register_available(0) ){
771796
@ <tr>
772797
@ <td></td>
773798
@ <td><input type="submit" name="self" value="Create A New Account">
774799
@ </tr>
800
+ }
801
+ if( login_self_password_reset_available() ){
802
+ @ <tr>
803
+ @ <td></td>
804
+ @ <td><input type="submit" name="pwreset" value="Reset My Password">
805
+ @ </tr>
775806
}
776807
@ </table>
777808
if( zAnonPw && !noAnon ){
778809
const char *zDecoded = captcha_decode(uSeed);
779810
int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
@@ -828,10 +859,243 @@
828859
@ </form>
829860
}
830861
}
831862
style_finish_page();
832863
}
864
+
865
+/*
866
+** Construct an appropriate URL suffix for the /resetpw page. The
867
+** suffix will be of the form:
868
+**
869
+** UID-TIMESTAMP-HASH
870
+**
871
+** Where UID and TIMESTAMP are the parameters to this function, and HASH
872
+** is constructed from information that is unique to the user in question
873
+** and which is not publicly available. In particular, the HASH includes
874
+** the existing user password. Thus, in order to construct a URL that can
875
+** change a password, an attacker must know the current password, in which
876
+** case the attacker does not need to construct the URL in order to take
877
+** over the account.
878
+**
879
+** Return a pointer to the resulting string in memory obtained
880
+** from fossil_malloc().
881
+*/
882
+char *login_resetpw_suffix(int uid, i64 timestamp){
883
+ char *zHash;
884
+ char *zInnerSql;
885
+ char *zResult;
886
+ extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*);
887
+ if( timestamp<=0 ){ timestamp = time(0); }
888
+ sqlite3_shathree_init(g.db, 0, 0);
889
+ if( db_table_exists("repository","subscriber") ){
890
+ zInnerSql = mprintf(
891
+ "SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode"
892
+ " FROM user LEFT JOIN subscriber ON suname=login"
893
+ " WHERE uid=%d", timestamp, uid);
894
+ }else{
895
+ zInnerSql = mprintf(
896
+ "SELECT %lld, login, pw, cookie, user.mtime, user.info"
897
+ " FROM user WHERE uid=%d", timestamp, uid);
898
+ }
899
+ zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql);
900
+ fossil_free(zInnerSql);
901
+ zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash);
902
+ if( strlen(zHash)<64 || strlen(zResult)<70 ){
903
+ /* This should never happen, but if it does, we don't want it to lead
904
+ ** to a security breach. */
905
+ fossil_panic("insecure password reset hash generated\n");
906
+ }
907
+ fossil_free(zHash);
908
+ return zResult;
909
+}
910
+
911
+/*
912
+** Check to see if the "name" query parameter is a valid resetpw suffix
913
+** for a user whose password we are allowed to reset. If it is, then return
914
+** the positive integer UID for that user. If the query parameter is not
915
+** valid, return 0.
916
+*/
917
+static int login_resetpw_suffix_is_valid(const char *zName){
918
+ int i, j;
919
+ int uid;
920
+ i64 timestamp;
921
+ i64 now;
922
+ char *zHash;
923
+ if( zName==0 || strlen(zName)<70 ) goto not_valid_suffix;
924
+ for(i=0; fossil_isxdigit(zName[i]); i++){}
925
+ if( i<1 || zName[i]!='-' ) goto not_valid_suffix;
926
+ for(j=i+1; fossil_isxdigit(zName[j]); j++){}
927
+ if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix;
928
+ uid = strtol(zName, 0, 16);
929
+ if( uid<=0 ) goto not_valid_suffix;
930
+ if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){
931
+ goto not_valid_suffix;
932
+ }
933
+ timestamp = strtoll(&zName[i+1], 0, 16);
934
+ now = time(0);
935
+ if( timestamp+3600 <= now ) goto not_valid_suffix;
936
+ zHash = login_resetpw_suffix(uid,timestamp);
937
+ if( fossil_strcmp(zHash, zName)!=0 ){
938
+ fossil_free(zHash);
939
+ goto not_valid_suffix;
940
+ }
941
+ fossil_free(zHash);
942
+ return uid;
943
+
944
+not_valid_suffix:
945
+ return 0;
946
+}
947
+
948
+/*
949
+** COMMAND: test-resetpw-url
950
+** Usage: fossil test-resetpw-url UID
951
+**
952
+** Generate and verify a /resetpw URL for user UID.
953
+**
954
+** This command is intended for unit testing the login_resetpw_suffix()
955
+** and login_resetpw_suffix_is_valid() functions.
956
+*/
957
+void test_resetpw_url(void){
958
+ char *zSuffix;
959
+ int uid;
960
+ int xuid;
961
+ char *zLogin;
962
+ int i;
963
+ db_find_and_open_repository(0, 0);
964
+ verify_all_options();
965
+ if( g.argc<3 ){
966
+ usage("UID ...");
967
+ }
968
+ for(i=2; i<g.argc; i++){
969
+ uid = atoi(g.argv[i]);
970
+ zSuffix = login_resetpw_suffix(uid, 0);
971
+ xuid = login_resetpw_suffix_is_valid(zSuffix);
972
+ if( xuid>0 ){
973
+ zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", xuid);
974
+ }else{
975
+ zLogin = 0;
976
+ }
977
+ fossil_print("/resetpw/%s %d (%s)\n",
978
+ zSuffix, xuid, zLogin ? zLogin : "???");
979
+ fossil_free(zSuffix);
980
+ fossil_free(zLogin);
981
+ }
982
+}
983
+
984
+/*
985
+** WEBPAGE: resetpw
986
+**
987
+** The URL format must be like this:
988
+**
989
+** /resetpw/UID-TIMESTAMP-HASH
990
+**
991
+** Where UID is the uid of the user whose password is to be reset,
992
+** TIMESTAMP is the unix timestamp when the request was made, and
993
+** HASH is a hash based on UID, TIMESTAMP, and other information that
994
+** is unavailable to an attacher.
995
+**
996
+** With no other arguments, a form is present which allows the user to
997
+** enter a new password. When the SUBMIT button is pressed, a POST request
998
+** back to the same URL that will change the password.
999
+*/
1000
+void login_resetpw(void){
1001
+ const char *zName;
1002
+ int uid;
1003
+ char *zRPW;
1004
+ const char *zNew1, *zNew2;
1005
+
1006
+ style_set_current_feature("resetpw");
1007
+ style_header("Reset Password");
1008
+ style_adunit_config(ADUNIT_OFF);
1009
+ zName = PD("name","");
1010
+ uid = login_resetpw_suffix_is_valid(zName);
1011
+ if( uid==0 ){
1012
+ @ <p><span class="loginError">
1013
+ @ This password-reset URL is invalid, probably because it has expired.
1014
+ @ Password-reset URLs have a short lifespan.
1015
+ @ </span></p>
1016
+ style_finish_page();
1017
+ sleep(1); /* Introduce a small delay on an invalid suffix as an
1018
+ ** extra defense against search attacks */
1019
+ return;
1020
+ }
1021
+ login_set_uid(uid, 0);
1022
+ if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){
1023
+ @ <p><span class="loginError">
1024
+ @ Cannot change the password for user <b>%h(g.zLogin)</b>.
1025
+ @ </span></p>
1026
+ style_finish_page();
1027
+ return;
1028
+ }
1029
+ if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
1030
+ if( fossil_strcmp(zNew1,zNew2)!=0 ){
1031
+ @ <p><span class="loginError">
1032
+ @ The two copies of your new passwords do not match.
1033
+ @ Try again.
1034
+ @ </span></p>
1035
+ }else{
1036
+ char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
1037
+ char *zChngPw;
1038
+ char *zErr;
1039
+ int rc;
1040
+
1041
+ /* vvvvvvv--- tag-20230106-2 ----vvvvvv
1042
+ **
1043
+ ** Replicate changes made below to tag-20230106-1
1044
+ */
1045
+ db_unprotect(PROTECT_USER);
1046
+ db_multi_exec(
1047
+ "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
1048
+ );
1049
+ zChngPw = mprintf(
1050
+ "UPDATE user"
1051
+ " SET pw=shared_secret(%Q,%Q,"
1052
+ " (SELECT value FROM config WHERE name='project-code'))"
1053
+ " WHERE login=%Q",
1054
+ zNew1, g.zLogin, g.zLogin
1055
+ );
1056
+ fossil_free(zNewPw);
1057
+ rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
1058
+ db_protect_pop();
1059
+ /*
1060
+ ** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^
1061
+ **
1062
+ ** Replicate changes above to tag-20230106-1
1063
+ */
1064
+
1065
+ if( rc ){
1066
+ @ <p><span class='loginError'>
1067
+ @ %s(zErr);
1068
+ @ </span></p>
1069
+ fossil_free(zErr);
1070
+ }else{
1071
+ @ <p>Password changed successfully. Go to the
1072
+ @ <a href="%R/login?u=%t(g.zLogin)">Login</a> page and log in
1073
+ @ using the new password to continue.
1074
+ @ </p>
1075
+ style_finish_page();
1076
+ return;
1077
+ }
1078
+ }
1079
+ }
1080
+ zRPW = fossil_random_password(12);
1081
+ @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
1082
+ form_begin(0, "%R/resetpw");
1083
+ @ <input type='hidden' name='name' value='%h(zName)'>
1084
+ @ <table>
1085
+ @ <tr><td class="form_label" id="newpw">New Password:</td>
1086
+ @ <td><input aria-labelledby="newpw" type="password" name="n1" \
1087
+ @ size="30" /> Suggestion: %z(zRPW)</td></tr>
1088
+ @ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
1089
+ @ <td><input aria-labledby="reppw" type="password" name="n2" \
1090
+ @ size="30" /></td></tr>
1091
+ @ <tr><td></td>
1092
+ @ <td><input type="submit" value="Change Password" /></td></tr>
1093
+ @ </table>
1094
+ @ </form>
1095
+ style_finish_page();
1096
+}
8331097
8341098
/*
8351099
** Attempt to find login credentials for user zLogin on a peer repository
8361100
** with project code zCode. Transfer those credentials to the local
8371101
** repository.
@@ -999,11 +1263,10 @@
9991263
void login_check_credentials(void){
10001264
int uid = 0; /* User id */
10011265
const char *zCookie; /* Text of the login cookie */
10021266
const char *zIpAddr; /* Raw IP address of the requestor */
10031267
const char *zCap = 0; /* Capability string */
1004
- const char *zPublicPages = 0; /* GLOB patterns of public pages */
10051268
const char *zLogin = 0; /* Login user for credentials */
10061269
10071270
/* Only run this check once. */
10081271
if( g.userUid!=0 ) return;
10091272
@@ -1139,10 +1402,21 @@
11391402
zCap = "";
11401403
}
11411404
sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none");
11421405
}
11431406
1407
+ login_set_uid(uid, zCap);
1408
+}
1409
+
1410
+/*
1411
+** Set the current logged in user to be uid. zCap is precomputed
1412
+** (override) capabilities. If zCap==0, then look up the capabilities
1413
+** in the USER table.
1414
+*/
1415
+int login_set_uid(int uid, const char *zCap){
1416
+ const char *zPublicPages = 0; /* GLOB patterns of public pages */
1417
+
11441418
/* At this point, we know that uid!=0. Find the privileges associated
11451419
** with user uid.
11461420
*/
11471421
assert( uid!=0 );
11481422
if( zCap==0 ){
@@ -1219,10 +1493,11 @@
12191493
if( glob_match(pGlob, zUri) ){
12201494
login_set_capabilities(db_get("default-perms", "u"), 0);
12211495
}
12221496
glob_free(pGlob);
12231497
}
1498
+ return g.zLogin!=0;
12241499
}
12251500
12261501
/*
12271502
** Memory of settings
12281503
*/
@@ -1557,10 +1832,75 @@
15571832
zUserID, zUserID, zUserID
15581833
);
15591834
return rc;
15601835
}
15611836
1837
+/*
1838
+** zEMail is an email address. (Example: "[email protected]".) This routine
1839
+** searches for a user or subscriber that has that email address. If the
1840
+** email address is used no-where in the system, return 0. If the email
1841
+** address is assigned to a particular user return the UID for that user.
1842
+** If the email address is used, but not by a particular user, return -1.
1843
+*/
1844
+static int email_address_in_use(const char *zEMail){
1845
+ int uid;
1846
+ uid = db_int(0,
1847
+ "SELECT uid FROM user"
1848
+ " WHERE info LIKE '%%<%q>%%'", zEMail);
1849
+ if( uid>0 ){
1850
+ if( db_exists("SELECT 1 FROM user WHERE uid=%d AND ("
1851
+ " cap GLOB '*[as]*' OR"
1852
+ " find_emailaddr(info)<>%Q COLLATE nocase)",
1853
+ uid, zEMail) ){
1854
+ uid = -1;
1855
+ }
1856
+ }
1857
+ if( uid==0 && alert_tables_exist() ){
1858
+ uid = db_int(0,
1859
+ "SELECT user.uid FROM subscriber JOIN user ON login=suname"
1860
+ " WHERE semail=%Q AND sverified", zEMail);
1861
+ if( uid ){
1862
+ if( db_exists("SELECT 1 FROM user WHERE uid=%d AND "
1863
+ " cap GLOB '*[as]*'",
1864
+ uid) ){
1865
+ uid = -1;
1866
+ }
1867
+ }
1868
+ }
1869
+ return uid;
1870
+}
1871
+
1872
+/*
1873
+** COMMAND: test-email-used
1874
+** Usage: fossil test-email-used EMAIL ...
1875
+**
1876
+** Given a list of email addresses, show the UID and LOGIN associated
1877
+** with each one.
1878
+*/
1879
+void test_email_used(void){
1880
+ int i;
1881
+ db_find_and_open_repository(0, 0);
1882
+ verify_all_options();
1883
+ if( g.argc<3 ){
1884
+ usage("EMAIL ...");
1885
+ }
1886
+ for(i=2; i<g.argc; i++){
1887
+ const char *zEMail = g.argv[i];
1888
+ int uid = email_address_in_use(zEMail);
1889
+ if( uid==0 ){
1890
+ fossil_print("%s: not used\n", zEMail);
1891
+ }else if( uid<0 ){
1892
+ fossil_print("%s: used but no password reset is available\n", zEMail);
1893
+ }else{
1894
+ char *zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
1895
+ fossil_print("%s: UID %d (%s)\n", zEMail, uid, zLogin);
1896
+ fossil_free(zLogin);
1897
+ }
1898
+ }
1899
+}
1900
+
1901
+
15621902
/*
15631903
** Check an email address and confirm that it is valid for self-registration.
15641904
** The email address is known already to be well-formed. Return true
15651905
** if the email address is on the allowed list.
15661906
**
@@ -1598,21 +1938,29 @@
15981938
const char *zDName;
15991939
unsigned int uSeed;
16001940
const char *zDecoded;
16011941
int iErrLine = -1;
16021942
const char *zErr = 0;
1943
+ int uid = 0; /* User id with the same email */
16031944
int captchaIsCorrect = 0; /* True on a correct captcha */
16041945
char *zCaptcha = ""; /* Value of the captcha text */
16051946
char *zPerms; /* Permissions for the default user */
16061947
int canDoAlerts = 0; /* True if receiving email alerts is possible */
16071948
int doAlerts = 0; /* True if subscription is wanted too */
1949
+
16081950
if( !db_get_boolean("self-register", 0) ){
16091951
style_header("Registration not possible");
16101952
@ <p>This project does not allow user self-registration. Please contact the
16111953
@ project administrator to obtain an account.</p>
16121954
style_finish_page();
16131955
return;
1956
+ }
1957
+ if( P("pwreset")!=0 && login_self_password_reset_available() ){
1958
+ /* The "Request Password Reset" button was pressed, so render the
1959
+ ** "Request Password Reset" page instead of this one. */
1960
+ login_reqpwreset_page();
1961
+ return;
16141962
}
16151963
zPerms = db_get("default-perms", "u");
16161964
16171965
/* Prompt the user for email alerts if this repository is configured for
16181966
** email alerts and if the default permissions include "7" */
@@ -1656,26 +2004,16 @@
16562004
iErrLine = 4;
16572005
zErr = "Password must be at least 6 characters long";
16582006
}else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
16592007
iErrLine = 5;
16602008
zErr = "Passwords do not match";
2009
+ }else if( (uid = email_address_in_use(zEAddr))!=0 ){
2010
+ iErrLine = 3;
2011
+ zErr = "This email address is already associated with a user";
16612012
}else if( login_self_choosen_userid_already_exists(zUserID) ){
16622013
iErrLine = 1;
16632014
zErr = "This User ID is already taken. Choose something different.";
1664
- }else if(
1665
- /* If the email is found anywhere in USER.INFO... */
1666
- db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr)
1667
- ||
1668
- /* Or if the email is a verify subscriber email with an associated
1669
- ** user... */
1670
- (alert_tables_exist() &&
1671
- db_exists(
1672
- "SELECT 1 FROM subscriber WHERE semail=%Q AND suname IS NOT NULL"
1673
- " AND sverified",zEAddr))
1674
- ){
1675
- iErrLine = 3;
1676
- zErr = "This email address is already claimed by another user";
16772015
}else{
16782016
/* If all of the tests above have passed, that means that the submitted
16792017
** form contains valid data and we can proceed to create the new login */
16802018
Blob sql;
16812019
int uid;
@@ -1809,11 +2147,17 @@
18092147
@ <td class="form_label" align="right" id="emaddr">Email Address:</td>
18102148
@ <td><input aria-labelledby="emaddr" type="text" name="ea" \
18112149
@ value="%h(zEAddr)" size="30"></td>
18122150
@ </tr>
18132151
if( iErrLine==3 ){
1814
- @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2152
+ @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span>
2153
+ if( uid>0 && login_self_password_reset_available() ){
2154
+ @ <br />
2155
+ @ <input type="submit" name="pwreset" \
2156
+ @ value="Request Password Reset For %h(zEAddr)">
2157
+ }
2158
+ @ </td></tr>
18152159
}
18162160
if( canDoAlerts ){
18172161
int a = atoi(PD("alerts","1"));
18182162
@ <tr>
18192163
@ <td class="form_label" align="right" id="emalrt">Email&nbsp;Alerts?</td>
@@ -1865,10 +2209,151 @@
18652209
@ </form>
18662210
style_finish_page();
18672211
18682212
free(zCaptcha);
18692213
}
2214
+
2215
+/*
2216
+** WEBPAGE: reqpwreset
2217
+**
2218
+** A web page to request a password reset.
2219
+*/
2220
+void login_reqpwreset_page(void){
2221
+ const char *zEAddr;
2222
+ const char *zDecoded;
2223
+ unsigned int uSeed;
2224
+ int iErrLine = -1;
2225
+ const char *zErr = 0;
2226
+ int uid = 0; /* User id with the email zEAddr */
2227
+ int captchaIsCorrect = 0; /* True on a correct captcha */
2228
+ char *zCaptcha = ""; /* Value of the captcha text */
2229
+
2230
+ if( !login_self_password_reset_available() ){
2231
+ style_header("Password reset not possible");
2232
+ @ <p>This project does not allow users to reset their own passwords.
2233
+ @ If you need a password reset, you will have to negotiate that directly
2234
+ @ with the project administrator.
2235
+ style_finish_page();
2236
+ return;
2237
+ }
2238
+ zEAddr = PDT("ea","");
2239
+
2240
+ /* Verify user imputs */
2241
+ if( !cgi_csrf_safe(1) || P("reqpwreset")==0 ){
2242
+ /* This is the initial display of the form. No processing or error
2243
+ ** checking is to be done. Fall through into the form display
2244
+ */
2245
+ }else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
2246
+ iErrLine = 2;
2247
+ zErr = "Incorrect CAPTCHA";
2248
+ }else if( zEAddr[0]==0 ){
2249
+ iErrLine = 1;
2250
+ zErr = "Required";
2251
+ }else if( email_address_is_valid(zEAddr,0)==0 ){
2252
+ iErrLine = 1;
2253
+ zErr = "Not a valid email address";
2254
+ }else if( authorized_subscription_email(zEAddr)==0 ){
2255
+ iErrLine = 1;
2256
+ zErr = "Not an authorized email address";
2257
+ }else if( (uid = email_address_in_use(zEAddr))<=0 ){
2258
+ iErrLine = 1;
2259
+ zErr = "This email address is not associated with a user who has "
2260
+ "password reset privileges.";
2261
+ }else if( login_set_uid(uid,0)==0 || g.perm.Admin || g.perm.Setup
2262
+ || !g.perm.Password ){
2263
+ iErrLine = 1;
2264
+ zErr = "This email address is not associated with a user who has "
2265
+ "password reset privileges.";
2266
+ }else{
2267
+
2268
+ /* If all of the tests above have passed, that means that the submitted
2269
+ ** form contains valid data and we can proceed to issue the password
2270
+ ** reset email. */
2271
+ Blob hdr, body;
2272
+ AlertSender *pSender;
2273
+ char *zUrl = login_resetpw_suffix(uid, 0);
2274
+ pSender = alert_sender_new(0,0);
2275
+ blob_init(&hdr,0,0);
2276
+ blob_init(&body,0,0);
2277
+ blob_appendf(&hdr, "To: <%s>\n", zEAddr);
2278
+ blob_appendf(&hdr, "Subject: Password reset for %s\n", g.zBaseURL);
2279
+ blob_appendf(&body,
2280
+ "Someone has requested to reset the password for user \"%s\"\n",
2281
+ g.zLogin);
2282
+ blob_appendf(&body, "at %s.\n\n", g.zBaseURL);
2283
+ blob_appendf(&body,
2284
+ "If you did not request this password reset, ignore\n"
2285
+ "this email\n\n");
2286
+ blob_appendf(&body,
2287
+ "To reset the password, visit the following link:\n\n"
2288
+ " %s/resetpw/%s\n\n", g.zBaseURL, zUrl);
2289
+ fossil_free(zUrl);
2290
+ alert_send(pSender, &hdr, &body, 0);
2291
+ style_header("Email Verification");
2292
+ if( pSender->zErr ){
2293
+ @ <h1>Internal Error</h1>
2294
+ @ <p>The following internal error was encountered while trying
2295
+ @ to send the confirmation email:
2296
+ @ <blockquote><pre>
2297
+ @ %h(pSender->zErr)
2298
+ @ </pre></blockquote>
2299
+ }else{
2300
+ @ <p>An email containing a hyperlink that can be used to reset
2301
+ @ your password has been sent to "%h(zEAddr)".</p>
2302
+ }
2303
+ alert_sender_free(pSender);
2304
+ style_finish_page();
2305
+ return;
2306
+ }
2307
+
2308
+ /* Prepare the captcha. */
2309
+ if( captchaIsCorrect ){
2310
+ uSeed = strtoul(P("captchaseed"),0,10);
2311
+ }else{
2312
+ uSeed = captcha_seed();
2313
+ }
2314
+ zDecoded = captcha_decode(uSeed);
2315
+ zCaptcha = captcha_render(zDecoded);
2316
+
2317
+ style_header("Request Password Reset");
2318
+ /* Print out the registration form. */
2319
+ g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */
2320
+ form_begin(0, "%R/reqpwreset");
2321
+ @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" />
2322
+ @ <p><input type="hidden" name="reqpwreset" value="1" />
2323
+ @ <table class="login_out">
2324
+ @ <tr>
2325
+ @ <td class="form_label" align="right" id="emaddr">Email Address:</td>
2326
+ @ <td><input aria-labelledby="emaddr" type="text" name="ea" \
2327
+ @ value="%h(zEAddr)" size="30"></td>
2328
+ @ </tr>
2329
+ if( iErrLine==1 ){
2330
+ @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2331
+ }
2332
+ @ <tr>
2333
+ @ <td class="form_label" align="right" id="cptcha">Captcha:</td>
2334
+ @ <td><input type="text" name="captcha" aria-labelledby="cptcha" \
2335
+ @ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
2336
+ captcha_speakit_button(uSeed, "Speak the captcha text");
2337
+ @ </td>
2338
+ @ </tr>
2339
+ if( iErrLine==2 ){
2340
+ @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2341
+ }
2342
+ @ <tr><td></td>
2343
+ @ <td><input type="submit" name="new" value="Request Password Reset"/>\
2344
+ @ </td></tr>
2345
+ @ </table>
2346
+ @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2347
+ @ %h(zCaptcha)
2348
+ @ </pre>
2349
+ @ Enter this 8-letter code in the "Captcha" box above.
2350
+ @ </td></tr></table></div>
2351
+ @ </form>
2352
+ style_finish_page();
2353
+ free(zCaptcha);
2354
+}
18702355
18712356
/*
18722357
** Run SQL on the repository database for every repository in our
18732358
** login group. The SQL is run in a separate database connection.
18742359
**
18752360
--- src/login.c
+++ src/login.c
@@ -510,10 +510,19 @@
510 zPattern = mprintf("%s/login*", g.zBaseURL);
511 rc = sqlite3_strglob(zPattern, zReferer)==0;
512 fossil_free(zPattern);
513 return rc;
514 }
 
 
 
 
 
 
 
 
 
515
516 /*
517 ** Return TRUE if self-registration is available. If the zNeeded
518 ** argument is not NULL, then only return true if self-registration is
519 ** available and any of the capabilities named in zNeeded are available
@@ -560,10 +569,16 @@
560 const int noAnon = P("noanon")!=0;
561 int rememberMe; /* If true, use persistent cookie, else
562 session cookie. Toggled per
563 checkbox. */
564
 
 
 
 
 
 
565 login_check_credentials();
566 fossil_redirect_to_https_if_needed(1);
567 sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
568 constant_time_cmp_function, 0, 0);
569 zUsername = P("u");
@@ -613,10 +628,14 @@
613 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
614 char *zChngPw;
615 char *zErr;
616 int rc;
617
 
 
 
 
618 db_unprotect(PROTECT_USER);
619 db_multi_exec(
620 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
621 );
622 zChngPw = mprintf(
@@ -627,10 +646,16 @@
627 zNew1, g.zLogin, g.zLogin
628 );
629 fossil_free(zNewPw);
630 rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
631 db_protect_pop();
 
 
 
 
 
 
632 if( rc ){
633 zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
634 fossil_free(zErr);
635 }else{
636 redirect_to_g();
@@ -770,10 +795,16 @@
770 if( !noAnon && login_self_register_available(0) ){
771 @ <tr>
772 @ <td></td>
773 @ <td><input type="submit" name="self" value="Create A New Account">
774 @ </tr>
 
 
 
 
 
 
775 }
776 @ </table>
777 if( zAnonPw && !noAnon ){
778 const char *zDecoded = captcha_decode(uSeed);
779 int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
@@ -828,10 +859,243 @@
828 @ </form>
829 }
830 }
831 style_finish_page();
832 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
834 /*
835 ** Attempt to find login credentials for user zLogin on a peer repository
836 ** with project code zCode. Transfer those credentials to the local
837 ** repository.
@@ -999,11 +1263,10 @@
999 void login_check_credentials(void){
1000 int uid = 0; /* User id */
1001 const char *zCookie; /* Text of the login cookie */
1002 const char *zIpAddr; /* Raw IP address of the requestor */
1003 const char *zCap = 0; /* Capability string */
1004 const char *zPublicPages = 0; /* GLOB patterns of public pages */
1005 const char *zLogin = 0; /* Login user for credentials */
1006
1007 /* Only run this check once. */
1008 if( g.userUid!=0 ) return;
1009
@@ -1139,10 +1402,21 @@
1139 zCap = "";
1140 }
1141 sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none");
1142 }
1143
 
 
 
 
 
 
 
 
 
 
 
1144 /* At this point, we know that uid!=0. Find the privileges associated
1145 ** with user uid.
1146 */
1147 assert( uid!=0 );
1148 if( zCap==0 ){
@@ -1219,10 +1493,11 @@
1219 if( glob_match(pGlob, zUri) ){
1220 login_set_capabilities(db_get("default-perms", "u"), 0);
1221 }
1222 glob_free(pGlob);
1223 }
 
1224 }
1225
1226 /*
1227 ** Memory of settings
1228 */
@@ -1557,10 +1832,75 @@
1557 zUserID, zUserID, zUserID
1558 );
1559 return rc;
1560 }
1561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1562 /*
1563 ** Check an email address and confirm that it is valid for self-registration.
1564 ** The email address is known already to be well-formed. Return true
1565 ** if the email address is on the allowed list.
1566 **
@@ -1598,21 +1938,29 @@
1598 const char *zDName;
1599 unsigned int uSeed;
1600 const char *zDecoded;
1601 int iErrLine = -1;
1602 const char *zErr = 0;
 
1603 int captchaIsCorrect = 0; /* True on a correct captcha */
1604 char *zCaptcha = ""; /* Value of the captcha text */
1605 char *zPerms; /* Permissions for the default user */
1606 int canDoAlerts = 0; /* True if receiving email alerts is possible */
1607 int doAlerts = 0; /* True if subscription is wanted too */
 
1608 if( !db_get_boolean("self-register", 0) ){
1609 style_header("Registration not possible");
1610 @ <p>This project does not allow user self-registration. Please contact the
1611 @ project administrator to obtain an account.</p>
1612 style_finish_page();
1613 return;
 
 
 
 
 
 
1614 }
1615 zPerms = db_get("default-perms", "u");
1616
1617 /* Prompt the user for email alerts if this repository is configured for
1618 ** email alerts and if the default permissions include "7" */
@@ -1656,26 +2004,16 @@
1656 iErrLine = 4;
1657 zErr = "Password must be at least 6 characters long";
1658 }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
1659 iErrLine = 5;
1660 zErr = "Passwords do not match";
 
 
 
1661 }else if( login_self_choosen_userid_already_exists(zUserID) ){
1662 iErrLine = 1;
1663 zErr = "This User ID is already taken. Choose something different.";
1664 }else if(
1665 /* If the email is found anywhere in USER.INFO... */
1666 db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr)
1667 ||
1668 /* Or if the email is a verify subscriber email with an associated
1669 ** user... */
1670 (alert_tables_exist() &&
1671 db_exists(
1672 "SELECT 1 FROM subscriber WHERE semail=%Q AND suname IS NOT NULL"
1673 " AND sverified",zEAddr))
1674 ){
1675 iErrLine = 3;
1676 zErr = "This email address is already claimed by another user";
1677 }else{
1678 /* If all of the tests above have passed, that means that the submitted
1679 ** form contains valid data and we can proceed to create the new login */
1680 Blob sql;
1681 int uid;
@@ -1809,11 +2147,17 @@
1809 @ <td class="form_label" align="right" id="emaddr">Email Address:</td>
1810 @ <td><input aria-labelledby="emaddr" type="text" name="ea" \
1811 @ value="%h(zEAddr)" size="30"></td>
1812 @ </tr>
1813 if( iErrLine==3 ){
1814 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
 
 
 
 
 
 
1815 }
1816 if( canDoAlerts ){
1817 int a = atoi(PD("alerts","1"));
1818 @ <tr>
1819 @ <td class="form_label" align="right" id="emalrt">Email&nbsp;Alerts?</td>
@@ -1865,10 +2209,151 @@
1865 @ </form>
1866 style_finish_page();
1867
1868 free(zCaptcha);
1869 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1870
1871 /*
1872 ** Run SQL on the repository database for every repository in our
1873 ** login group. The SQL is run in a separate database connection.
1874 **
1875
--- src/login.c
+++ src/login.c
@@ -510,10 +510,19 @@
510 zPattern = mprintf("%s/login*", g.zBaseURL);
511 rc = sqlite3_strglob(zPattern, zReferer)==0;
512 fossil_free(zPattern);
513 return rc;
514 }
515
516 /*
517 ** Return true if users are allowed to reset their own passwords.
518 */
519 int login_self_password_reset_available(void){
520 if( !db_get_boolean("self-pw-reset",0) ) return 0;
521 if( !alert_tables_exist() ) return 0;
522 return 1;
523 }
524
525 /*
526 ** Return TRUE if self-registration is available. If the zNeeded
527 ** argument is not NULL, then only return true if self-registration is
528 ** available and any of the capabilities named in zNeeded are available
@@ -560,10 +569,16 @@
569 const int noAnon = P("noanon")!=0;
570 int rememberMe; /* If true, use persistent cookie, else
571 session cookie. Toggled per
572 checkbox. */
573
574 if( P("pwreset")!=0 && login_self_password_reset_available() ){
575 /* If the "Reset Password" button in the form was pressed, render
576 ** the Request Password Reset page in place of this one. */
577 login_reqpwreset_page();
578 return;
579 }
580 login_check_credentials();
581 fossil_redirect_to_https_if_needed(1);
582 sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
583 constant_time_cmp_function, 0, 0);
584 zUsername = P("u");
@@ -613,10 +628,14 @@
628 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
629 char *zChngPw;
630 char *zErr;
631 int rc;
632
633 /* vvvvvvv--- tag-20230106-1 ----vvvvvv
634 **
635 ** Replicate changes made below to tag-20230106-2
636 */
637 db_unprotect(PROTECT_USER);
638 db_multi_exec(
639 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
640 );
641 zChngPw = mprintf(
@@ -627,10 +646,16 @@
646 zNew1, g.zLogin, g.zLogin
647 );
648 fossil_free(zNewPw);
649 rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
650 db_protect_pop();
651 /*
652 ** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^
653 **
654 ** Replicate changes above to tag-20230106-2
655 */
656
657 if( rc ){
658 zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
659 fossil_free(zErr);
660 }else{
661 redirect_to_g();
@@ -770,10 +795,16 @@
795 if( !noAnon && login_self_register_available(0) ){
796 @ <tr>
797 @ <td></td>
798 @ <td><input type="submit" name="self" value="Create A New Account">
799 @ </tr>
800 }
801 if( login_self_password_reset_available() ){
802 @ <tr>
803 @ <td></td>
804 @ <td><input type="submit" name="pwreset" value="Reset My Password">
805 @ </tr>
806 }
807 @ </table>
808 if( zAnonPw && !noAnon ){
809 const char *zDecoded = captcha_decode(uSeed);
810 int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
@@ -828,10 +859,243 @@
859 @ </form>
860 }
861 }
862 style_finish_page();
863 }
864
865 /*
866 ** Construct an appropriate URL suffix for the /resetpw page. The
867 ** suffix will be of the form:
868 **
869 ** UID-TIMESTAMP-HASH
870 **
871 ** Where UID and TIMESTAMP are the parameters to this function, and HASH
872 ** is constructed from information that is unique to the user in question
873 ** and which is not publicly available. In particular, the HASH includes
874 ** the existing user password. Thus, in order to construct a URL that can
875 ** change a password, an attacker must know the current password, in which
876 ** case the attacker does not need to construct the URL in order to take
877 ** over the account.
878 **
879 ** Return a pointer to the resulting string in memory obtained
880 ** from fossil_malloc().
881 */
882 char *login_resetpw_suffix(int uid, i64 timestamp){
883 char *zHash;
884 char *zInnerSql;
885 char *zResult;
886 extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*);
887 if( timestamp<=0 ){ timestamp = time(0); }
888 sqlite3_shathree_init(g.db, 0, 0);
889 if( db_table_exists("repository","subscriber") ){
890 zInnerSql = mprintf(
891 "SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode"
892 " FROM user LEFT JOIN subscriber ON suname=login"
893 " WHERE uid=%d", timestamp, uid);
894 }else{
895 zInnerSql = mprintf(
896 "SELECT %lld, login, pw, cookie, user.mtime, user.info"
897 " FROM user WHERE uid=%d", timestamp, uid);
898 }
899 zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql);
900 fossil_free(zInnerSql);
901 zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash);
902 if( strlen(zHash)<64 || strlen(zResult)<70 ){
903 /* This should never happen, but if it does, we don't want it to lead
904 ** to a security breach. */
905 fossil_panic("insecure password reset hash generated\n");
906 }
907 fossil_free(zHash);
908 return zResult;
909 }
910
911 /*
912 ** Check to see if the "name" query parameter is a valid resetpw suffix
913 ** for a user whose password we are allowed to reset. If it is, then return
914 ** the positive integer UID for that user. If the query parameter is not
915 ** valid, return 0.
916 */
917 static int login_resetpw_suffix_is_valid(const char *zName){
918 int i, j;
919 int uid;
920 i64 timestamp;
921 i64 now;
922 char *zHash;
923 if( zName==0 || strlen(zName)<70 ) goto not_valid_suffix;
924 for(i=0; fossil_isxdigit(zName[i]); i++){}
925 if( i<1 || zName[i]!='-' ) goto not_valid_suffix;
926 for(j=i+1; fossil_isxdigit(zName[j]); j++){}
927 if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix;
928 uid = strtol(zName, 0, 16);
929 if( uid<=0 ) goto not_valid_suffix;
930 if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){
931 goto not_valid_suffix;
932 }
933 timestamp = strtoll(&zName[i+1], 0, 16);
934 now = time(0);
935 if( timestamp+3600 <= now ) goto not_valid_suffix;
936 zHash = login_resetpw_suffix(uid,timestamp);
937 if( fossil_strcmp(zHash, zName)!=0 ){
938 fossil_free(zHash);
939 goto not_valid_suffix;
940 }
941 fossil_free(zHash);
942 return uid;
943
944 not_valid_suffix:
945 return 0;
946 }
947
948 /*
949 ** COMMAND: test-resetpw-url
950 ** Usage: fossil test-resetpw-url UID
951 **
952 ** Generate and verify a /resetpw URL for user UID.
953 **
954 ** This command is intended for unit testing the login_resetpw_suffix()
955 ** and login_resetpw_suffix_is_valid() functions.
956 */
957 void test_resetpw_url(void){
958 char *zSuffix;
959 int uid;
960 int xuid;
961 char *zLogin;
962 int i;
963 db_find_and_open_repository(0, 0);
964 verify_all_options();
965 if( g.argc<3 ){
966 usage("UID ...");
967 }
968 for(i=2; i<g.argc; i++){
969 uid = atoi(g.argv[i]);
970 zSuffix = login_resetpw_suffix(uid, 0);
971 xuid = login_resetpw_suffix_is_valid(zSuffix);
972 if( xuid>0 ){
973 zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", xuid);
974 }else{
975 zLogin = 0;
976 }
977 fossil_print("/resetpw/%s %d (%s)\n",
978 zSuffix, xuid, zLogin ? zLogin : "???");
979 fossil_free(zSuffix);
980 fossil_free(zLogin);
981 }
982 }
983
984 /*
985 ** WEBPAGE: resetpw
986 **
987 ** The URL format must be like this:
988 **
989 ** /resetpw/UID-TIMESTAMP-HASH
990 **
991 ** Where UID is the uid of the user whose password is to be reset,
992 ** TIMESTAMP is the unix timestamp when the request was made, and
993 ** HASH is a hash based on UID, TIMESTAMP, and other information that
994 ** is unavailable to an attacher.
995 **
996 ** With no other arguments, a form is present which allows the user to
997 ** enter a new password. When the SUBMIT button is pressed, a POST request
998 ** back to the same URL that will change the password.
999 */
1000 void login_resetpw(void){
1001 const char *zName;
1002 int uid;
1003 char *zRPW;
1004 const char *zNew1, *zNew2;
1005
1006 style_set_current_feature("resetpw");
1007 style_header("Reset Password");
1008 style_adunit_config(ADUNIT_OFF);
1009 zName = PD("name","");
1010 uid = login_resetpw_suffix_is_valid(zName);
1011 if( uid==0 ){
1012 @ <p><span class="loginError">
1013 @ This password-reset URL is invalid, probably because it has expired.
1014 @ Password-reset URLs have a short lifespan.
1015 @ </span></p>
1016 style_finish_page();
1017 sleep(1); /* Introduce a small delay on an invalid suffix as an
1018 ** extra defense against search attacks */
1019 return;
1020 }
1021 login_set_uid(uid, 0);
1022 if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){
1023 @ <p><span class="loginError">
1024 @ Cannot change the password for user <b>%h(g.zLogin)</b>.
1025 @ </span></p>
1026 style_finish_page();
1027 return;
1028 }
1029 if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
1030 if( fossil_strcmp(zNew1,zNew2)!=0 ){
1031 @ <p><span class="loginError">
1032 @ The two copies of your new passwords do not match.
1033 @ Try again.
1034 @ </span></p>
1035 }else{
1036 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
1037 char *zChngPw;
1038 char *zErr;
1039 int rc;
1040
1041 /* vvvvvvv--- tag-20230106-2 ----vvvvvv
1042 **
1043 ** Replicate changes made below to tag-20230106-1
1044 */
1045 db_unprotect(PROTECT_USER);
1046 db_multi_exec(
1047 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
1048 );
1049 zChngPw = mprintf(
1050 "UPDATE user"
1051 " SET pw=shared_secret(%Q,%Q,"
1052 " (SELECT value FROM config WHERE name='project-code'))"
1053 " WHERE login=%Q",
1054 zNew1, g.zLogin, g.zLogin
1055 );
1056 fossil_free(zNewPw);
1057 rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
1058 db_protect_pop();
1059 /*
1060 ** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^
1061 **
1062 ** Replicate changes above to tag-20230106-1
1063 */
1064
1065 if( rc ){
1066 @ <p><span class='loginError'>
1067 @ %s(zErr);
1068 @ </span></p>
1069 fossil_free(zErr);
1070 }else{
1071 @ <p>Password changed successfully. Go to the
1072 @ <a href="%R/login?u=%t(g.zLogin)">Login</a> page and log in
1073 @ using the new password to continue.
1074 @ </p>
1075 style_finish_page();
1076 return;
1077 }
1078 }
1079 }
1080 zRPW = fossil_random_password(12);
1081 @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
1082 form_begin(0, "%R/resetpw");
1083 @ <input type='hidden' name='name' value='%h(zName)'>
1084 @ <table>
1085 @ <tr><td class="form_label" id="newpw">New Password:</td>
1086 @ <td><input aria-labelledby="newpw" type="password" name="n1" \
1087 @ size="30" /> Suggestion: %z(zRPW)</td></tr>
1088 @ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
1089 @ <td><input aria-labledby="reppw" type="password" name="n2" \
1090 @ size="30" /></td></tr>
1091 @ <tr><td></td>
1092 @ <td><input type="submit" value="Change Password" /></td></tr>
1093 @ </table>
1094 @ </form>
1095 style_finish_page();
1096 }
1097
1098 /*
1099 ** Attempt to find login credentials for user zLogin on a peer repository
1100 ** with project code zCode. Transfer those credentials to the local
1101 ** repository.
@@ -999,11 +1263,10 @@
1263 void login_check_credentials(void){
1264 int uid = 0; /* User id */
1265 const char *zCookie; /* Text of the login cookie */
1266 const char *zIpAddr; /* Raw IP address of the requestor */
1267 const char *zCap = 0; /* Capability string */
 
1268 const char *zLogin = 0; /* Login user for credentials */
1269
1270 /* Only run this check once. */
1271 if( g.userUid!=0 ) return;
1272
@@ -1139,10 +1402,21 @@
1402 zCap = "";
1403 }
1404 sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none");
1405 }
1406
1407 login_set_uid(uid, zCap);
1408 }
1409
1410 /*
1411 ** Set the current logged in user to be uid. zCap is precomputed
1412 ** (override) capabilities. If zCap==0, then look up the capabilities
1413 ** in the USER table.
1414 */
1415 int login_set_uid(int uid, const char *zCap){
1416 const char *zPublicPages = 0; /* GLOB patterns of public pages */
1417
1418 /* At this point, we know that uid!=0. Find the privileges associated
1419 ** with user uid.
1420 */
1421 assert( uid!=0 );
1422 if( zCap==0 ){
@@ -1219,10 +1493,11 @@
1493 if( glob_match(pGlob, zUri) ){
1494 login_set_capabilities(db_get("default-perms", "u"), 0);
1495 }
1496 glob_free(pGlob);
1497 }
1498 return g.zLogin!=0;
1499 }
1500
1501 /*
1502 ** Memory of settings
1503 */
@@ -1557,10 +1832,75 @@
1832 zUserID, zUserID, zUserID
1833 );
1834 return rc;
1835 }
1836
1837 /*
1838 ** zEMail is an email address. (Example: "[email protected]".) This routine
1839 ** searches for a user or subscriber that has that email address. If the
1840 ** email address is used no-where in the system, return 0. If the email
1841 ** address is assigned to a particular user return the UID for that user.
1842 ** If the email address is used, but not by a particular user, return -1.
1843 */
1844 static int email_address_in_use(const char *zEMail){
1845 int uid;
1846 uid = db_int(0,
1847 "SELECT uid FROM user"
1848 " WHERE info LIKE '%%<%q>%%'", zEMail);
1849 if( uid>0 ){
1850 if( db_exists("SELECT 1 FROM user WHERE uid=%d AND ("
1851 " cap GLOB '*[as]*' OR"
1852 " find_emailaddr(info)<>%Q COLLATE nocase)",
1853 uid, zEMail) ){
1854 uid = -1;
1855 }
1856 }
1857 if( uid==0 && alert_tables_exist() ){
1858 uid = db_int(0,
1859 "SELECT user.uid FROM subscriber JOIN user ON login=suname"
1860 " WHERE semail=%Q AND sverified", zEMail);
1861 if( uid ){
1862 if( db_exists("SELECT 1 FROM user WHERE uid=%d AND "
1863 " cap GLOB '*[as]*'",
1864 uid) ){
1865 uid = -1;
1866 }
1867 }
1868 }
1869 return uid;
1870 }
1871
1872 /*
1873 ** COMMAND: test-email-used
1874 ** Usage: fossil test-email-used EMAIL ...
1875 **
1876 ** Given a list of email addresses, show the UID and LOGIN associated
1877 ** with each one.
1878 */
1879 void test_email_used(void){
1880 int i;
1881 db_find_and_open_repository(0, 0);
1882 verify_all_options();
1883 if( g.argc<3 ){
1884 usage("EMAIL ...");
1885 }
1886 for(i=2; i<g.argc; i++){
1887 const char *zEMail = g.argv[i];
1888 int uid = email_address_in_use(zEMail);
1889 if( uid==0 ){
1890 fossil_print("%s: not used\n", zEMail);
1891 }else if( uid<0 ){
1892 fossil_print("%s: used but no password reset is available\n", zEMail);
1893 }else{
1894 char *zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
1895 fossil_print("%s: UID %d (%s)\n", zEMail, uid, zLogin);
1896 fossil_free(zLogin);
1897 }
1898 }
1899 }
1900
1901
1902 /*
1903 ** Check an email address and confirm that it is valid for self-registration.
1904 ** The email address is known already to be well-formed. Return true
1905 ** if the email address is on the allowed list.
1906 **
@@ -1598,21 +1938,29 @@
1938 const char *zDName;
1939 unsigned int uSeed;
1940 const char *zDecoded;
1941 int iErrLine = -1;
1942 const char *zErr = 0;
1943 int uid = 0; /* User id with the same email */
1944 int captchaIsCorrect = 0; /* True on a correct captcha */
1945 char *zCaptcha = ""; /* Value of the captcha text */
1946 char *zPerms; /* Permissions for the default user */
1947 int canDoAlerts = 0; /* True if receiving email alerts is possible */
1948 int doAlerts = 0; /* True if subscription is wanted too */
1949
1950 if( !db_get_boolean("self-register", 0) ){
1951 style_header("Registration not possible");
1952 @ <p>This project does not allow user self-registration. Please contact the
1953 @ project administrator to obtain an account.</p>
1954 style_finish_page();
1955 return;
1956 }
1957 if( P("pwreset")!=0 && login_self_password_reset_available() ){
1958 /* The "Request Password Reset" button was pressed, so render the
1959 ** "Request Password Reset" page instead of this one. */
1960 login_reqpwreset_page();
1961 return;
1962 }
1963 zPerms = db_get("default-perms", "u");
1964
1965 /* Prompt the user for email alerts if this repository is configured for
1966 ** email alerts and if the default permissions include "7" */
@@ -1656,26 +2004,16 @@
2004 iErrLine = 4;
2005 zErr = "Password must be at least 6 characters long";
2006 }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
2007 iErrLine = 5;
2008 zErr = "Passwords do not match";
2009 }else if( (uid = email_address_in_use(zEAddr))!=0 ){
2010 iErrLine = 3;
2011 zErr = "This email address is already associated with a user";
2012 }else if( login_self_choosen_userid_already_exists(zUserID) ){
2013 iErrLine = 1;
2014 zErr = "This User ID is already taken. Choose something different.";
 
 
 
 
 
 
 
 
 
 
 
 
 
2015 }else{
2016 /* If all of the tests above have passed, that means that the submitted
2017 ** form contains valid data and we can proceed to create the new login */
2018 Blob sql;
2019 int uid;
@@ -1809,11 +2147,17 @@
2147 @ <td class="form_label" align="right" id="emaddr">Email Address:</td>
2148 @ <td><input aria-labelledby="emaddr" type="text" name="ea" \
2149 @ value="%h(zEAddr)" size="30"></td>
2150 @ </tr>
2151 if( iErrLine==3 ){
2152 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span>
2153 if( uid>0 && login_self_password_reset_available() ){
2154 @ <br />
2155 @ <input type="submit" name="pwreset" \
2156 @ value="Request Password Reset For %h(zEAddr)">
2157 }
2158 @ </td></tr>
2159 }
2160 if( canDoAlerts ){
2161 int a = atoi(PD("alerts","1"));
2162 @ <tr>
2163 @ <td class="form_label" align="right" id="emalrt">Email&nbsp;Alerts?</td>
@@ -1865,10 +2209,151 @@
2209 @ </form>
2210 style_finish_page();
2211
2212 free(zCaptcha);
2213 }
2214
2215 /*
2216 ** WEBPAGE: reqpwreset
2217 **
2218 ** A web page to request a password reset.
2219 */
2220 void login_reqpwreset_page(void){
2221 const char *zEAddr;
2222 const char *zDecoded;
2223 unsigned int uSeed;
2224 int iErrLine = -1;
2225 const char *zErr = 0;
2226 int uid = 0; /* User id with the email zEAddr */
2227 int captchaIsCorrect = 0; /* True on a correct captcha */
2228 char *zCaptcha = ""; /* Value of the captcha text */
2229
2230 if( !login_self_password_reset_available() ){
2231 style_header("Password reset not possible");
2232 @ <p>This project does not allow users to reset their own passwords.
2233 @ If you need a password reset, you will have to negotiate that directly
2234 @ with the project administrator.
2235 style_finish_page();
2236 return;
2237 }
2238 zEAddr = PDT("ea","");
2239
2240 /* Verify user imputs */
2241 if( !cgi_csrf_safe(1) || P("reqpwreset")==0 ){
2242 /* This is the initial display of the form. No processing or error
2243 ** checking is to be done. Fall through into the form display
2244 */
2245 }else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
2246 iErrLine = 2;
2247 zErr = "Incorrect CAPTCHA";
2248 }else if( zEAddr[0]==0 ){
2249 iErrLine = 1;
2250 zErr = "Required";
2251 }else if( email_address_is_valid(zEAddr,0)==0 ){
2252 iErrLine = 1;
2253 zErr = "Not a valid email address";
2254 }else if( authorized_subscription_email(zEAddr)==0 ){
2255 iErrLine = 1;
2256 zErr = "Not an authorized email address";
2257 }else if( (uid = email_address_in_use(zEAddr))<=0 ){
2258 iErrLine = 1;
2259 zErr = "This email address is not associated with a user who has "
2260 "password reset privileges.";
2261 }else if( login_set_uid(uid,0)==0 || g.perm.Admin || g.perm.Setup
2262 || !g.perm.Password ){
2263 iErrLine = 1;
2264 zErr = "This email address is not associated with a user who has "
2265 "password reset privileges.";
2266 }else{
2267
2268 /* If all of the tests above have passed, that means that the submitted
2269 ** form contains valid data and we can proceed to issue the password
2270 ** reset email. */
2271 Blob hdr, body;
2272 AlertSender *pSender;
2273 char *zUrl = login_resetpw_suffix(uid, 0);
2274 pSender = alert_sender_new(0,0);
2275 blob_init(&hdr,0,0);
2276 blob_init(&body,0,0);
2277 blob_appendf(&hdr, "To: <%s>\n", zEAddr);
2278 blob_appendf(&hdr, "Subject: Password reset for %s\n", g.zBaseURL);
2279 blob_appendf(&body,
2280 "Someone has requested to reset the password for user \"%s\"\n",
2281 g.zLogin);
2282 blob_appendf(&body, "at %s.\n\n", g.zBaseURL);
2283 blob_appendf(&body,
2284 "If you did not request this password reset, ignore\n"
2285 "this email\n\n");
2286 blob_appendf(&body,
2287 "To reset the password, visit the following link:\n\n"
2288 " %s/resetpw/%s\n\n", g.zBaseURL, zUrl);
2289 fossil_free(zUrl);
2290 alert_send(pSender, &hdr, &body, 0);
2291 style_header("Email Verification");
2292 if( pSender->zErr ){
2293 @ <h1>Internal Error</h1>
2294 @ <p>The following internal error was encountered while trying
2295 @ to send the confirmation email:
2296 @ <blockquote><pre>
2297 @ %h(pSender->zErr)
2298 @ </pre></blockquote>
2299 }else{
2300 @ <p>An email containing a hyperlink that can be used to reset
2301 @ your password has been sent to "%h(zEAddr)".</p>
2302 }
2303 alert_sender_free(pSender);
2304 style_finish_page();
2305 return;
2306 }
2307
2308 /* Prepare the captcha. */
2309 if( captchaIsCorrect ){
2310 uSeed = strtoul(P("captchaseed"),0,10);
2311 }else{
2312 uSeed = captcha_seed();
2313 }
2314 zDecoded = captcha_decode(uSeed);
2315 zCaptcha = captcha_render(zDecoded);
2316
2317 style_header("Request Password Reset");
2318 /* Print out the registration form. */
2319 g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */
2320 form_begin(0, "%R/reqpwreset");
2321 @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" />
2322 @ <p><input type="hidden" name="reqpwreset" value="1" />
2323 @ <table class="login_out">
2324 @ <tr>
2325 @ <td class="form_label" align="right" id="emaddr">Email Address:</td>
2326 @ <td><input aria-labelledby="emaddr" type="text" name="ea" \
2327 @ value="%h(zEAddr)" size="30"></td>
2328 @ </tr>
2329 if( iErrLine==1 ){
2330 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2331 }
2332 @ <tr>
2333 @ <td class="form_label" align="right" id="cptcha">Captcha:</td>
2334 @ <td><input type="text" name="captcha" aria-labelledby="cptcha" \
2335 @ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
2336 captcha_speakit_button(uSeed, "Speak the captcha text");
2337 @ </td>
2338 @ </tr>
2339 if( iErrLine==2 ){
2340 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2341 }
2342 @ <tr><td></td>
2343 @ <td><input type="submit" name="new" value="Request Password Reset"/>\
2344 @ </td></tr>
2345 @ </table>
2346 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2347 @ %h(zCaptcha)
2348 @ </pre>
2349 @ Enter this 8-letter code in the "Captcha" box above.
2350 @ </td></tr></table></div>
2351 @ </form>
2352 style_finish_page();
2353 free(zCaptcha);
2354 }
2355
2356 /*
2357 ** Run SQL on the repository database for every repository in our
2358 ** login group. The SQL is run in a separate database connection.
2359 **
2360
--- src/setup.c
+++ src/setup.c
@@ -594,10 +594,19 @@
594594
@ A self-registration creates a new entry in the USER table and
595595
@ perhaps also in the SUBSCRIBER table if email notification is
596596
@ enabled.
597597
@ (Property: "self-register")</p>
598598
599
+ @ <hr />
600
+ onoff_attribute("Allow users to reset their own passwords",
601
+ "self-pw-reset", "selfpw", 0, 0);
602
+ @ <p>Allow users to request that an email contains a hyperlink to a
603
+ @ password reset page be sent to their email address of record. This
604
+ @ enables forgetful users to recover their forgotten passwords without
605
+ @ administrator intervention.
606
+ @ (Property: "self-pw-reset")</p>
607
+
599608
@ <hr />
600609
onoff_attribute("Email verification required for self-registration",
601610
"selfreg-verify", "sfverify", 0, 0);
602611
@ <p>If enabled, self-registration creates a new entry in the USER table
603612
@ with only capabilities "7". The default user capabilities are not
604613
--- src/setup.c
+++ src/setup.c
@@ -594,10 +594,19 @@
594 @ A self-registration creates a new entry in the USER table and
595 @ perhaps also in the SUBSCRIBER table if email notification is
596 @ enabled.
597 @ (Property: "self-register")</p>
598
 
 
 
 
 
 
 
 
 
599 @ <hr />
600 onoff_attribute("Email verification required for self-registration",
601 "selfreg-verify", "sfverify", 0, 0);
602 @ <p>If enabled, self-registration creates a new entry in the USER table
603 @ with only capabilities "7". The default user capabilities are not
604
--- src/setup.c
+++ src/setup.c
@@ -594,10 +594,19 @@
594 @ A self-registration creates a new entry in the USER table and
595 @ perhaps also in the SUBSCRIBER table if email notification is
596 @ enabled.
597 @ (Property: "self-register")</p>
598
599 @ <hr />
600 onoff_attribute("Allow users to reset their own passwords",
601 "self-pw-reset", "selfpw", 0, 0);
602 @ <p>Allow users to request that an email contains a hyperlink to a
603 @ password reset page be sent to their email address of record. This
604 @ enables forgetful users to recover their forgotten passwords without
605 @ administrator intervention.
606 @ (Property: "self-pw-reset")</p>
607
608 @ <hr />
609 onoff_attribute("Email verification required for self-registration",
610 "selfreg-verify", "sfverify", 0, 0);
611 @ <p>If enabled, self-registration creates a new entry in the USER table
612 @ with only capabilities "7". The default user capabilities are not
613

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button