Fossil SCM
Add the /resetpw web page. The name argument must contain a hash that proves knowledge of the old password and that limits the valid lifetime of the argument.
Commit
ac86dfa085cd5eed9b3c8d94a50d5a6ccf083877c96d9cdf66e83329343dd666
Parent
0bf2eaaf6528f5d…
2 files changed
+1
+229
-1
+1
| --- src/blob.c | ||
| +++ src/blob.c | ||
| @@ -124,10 +124,11 @@ | ||
| 124 | 124 | ** Other replacements for ctype.h functions. |
| 125 | 125 | */ |
| 126 | 126 | int fossil_islower(char c){ return c>='a' && c<='z'; } |
| 127 | 127 | int fossil_isupper(char c){ return c>='A' && c<='Z'; } |
| 128 | 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'); } | |
| 129 | 130 | int fossil_tolower(char c){ |
| 130 | 131 | return fossil_isupper(c) ? c - 'A' + 'a' : c; |
| 131 | 132 | } |
| 132 | 133 | int fossil_toupper(char c){ |
| 133 | 134 | return fossil_islower(c) ? c - 'a' + 'A' : c; |
| 134 | 135 |
| --- 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 |
+229
-1
| --- src/login.c | ||
| +++ src/login.c | ||
| @@ -613,10 +613,14 @@ | ||
| 613 | 613 | char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); |
| 614 | 614 | char *zChngPw; |
| 615 | 615 | char *zErr; |
| 616 | 616 | int rc; |
| 617 | 617 | |
| 618 | + /* vvvvvvv--- tag-20230106-1 ----vvvvvv | |
| 619 | + ** | |
| 620 | + ** Replicate changes made below to tag-20230106-2 | |
| 621 | + */ | |
| 618 | 622 | db_unprotect(PROTECT_USER); |
| 619 | 623 | db_multi_exec( |
| 620 | 624 | "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 621 | 625 | ); |
| 622 | 626 | zChngPw = mprintf( |
| @@ -627,10 +631,16 @@ | ||
| 627 | 631 | zNew1, g.zLogin, g.zLogin |
| 628 | 632 | ); |
| 629 | 633 | fossil_free(zNewPw); |
| 630 | 634 | rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr); |
| 631 | 635 | db_protect_pop(); |
| 636 | + /* | |
| 637 | + ** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^ | |
| 638 | + ** | |
| 639 | + ** Replicate changes above to tag-20230106-2 | |
| 640 | + */ | |
| 641 | + | |
| 632 | 642 | if( rc ){ |
| 633 | 643 | zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr); |
| 634 | 644 | fossil_free(zErr); |
| 635 | 645 | }else{ |
| 636 | 646 | redirect_to_g(); |
| @@ -828,10 +838,218 @@ | ||
| 828 | 838 | @ </form> |
| 829 | 839 | } |
| 830 | 840 | } |
| 831 | 841 | style_finish_page(); |
| 832 | 842 | } |
| 843 | + | |
| 844 | +/* | |
| 845 | +** Construct an appropriate URL suffix for the /resetpw page. The | |
| 846 | +** suffix will be of the form: | |
| 847 | +** | |
| 848 | +** UID-TIMESTAMP-HASH | |
| 849 | +** | |
| 850 | +** Where UID and TIMESTAMP are the parameters to this function, and HASH | |
| 851 | +** is constructed from information that is unique to the user in question | |
| 852 | +** and which is not publicly available. In particular, the HASH includes | |
| 853 | +** the existing user password. Thus, in order to construct a URL that can | |
| 854 | +** change a password, the attacker must know the current password, in which | |
| 855 | +** case that do not need to construct the URL in order to take over the | |
| 856 | +** account. | |
| 857 | +** | |
| 858 | +** Return a pointer to the resulting string in memory obtained | |
| 859 | +** from fossil_malloc(). | |
| 860 | +*/ | |
| 861 | +char *login_resetpw_suffix(int uid, i64 timestamp){ | |
| 862 | + char *zHash; | |
| 863 | + char *zInnerSql; | |
| 864 | + char *zResult; | |
| 865 | + extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*); | |
| 866 | + if( timestamp<=0 ){ timestamp = time(0); } | |
| 867 | + sqlite3_shathree_init(g.db, 0, 0); | |
| 868 | + if( db_table_exists("repository","subscriber") ){ | |
| 869 | + zInnerSql = mprintf( | |
| 870 | + "SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode" | |
| 871 | + " FROM user LEFT JOIN subscriber ON suname=login" | |
| 872 | + " WHERE uid=%d", timestamp, uid); | |
| 873 | + }else{ | |
| 874 | + zInnerSql = mprintf( | |
| 875 | + "SELECT %lld, login, pw, cookie, user.mtime, user.info" | |
| 876 | + " FROM user WHERE uid=%d", timestamp, uid); | |
| 877 | + } | |
| 878 | + zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql); | |
| 879 | + fossil_free(zInnerSql); | |
| 880 | + zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash); | |
| 881 | + fossil_free(zHash); | |
| 882 | + return zResult; | |
| 883 | +} | |
| 884 | + | |
| 885 | +/* | |
| 886 | +** Check to see if the "name" query parameter is a valid resetpw suffix | |
| 887 | +** for a user whose password we are allowed to reset. If it is, then return | |
| 888 | +** the positive integer UID for that user. If the query parameter is not | |
| 889 | +** valid, return 0. | |
| 890 | +*/ | |
| 891 | +static int login_resetpw_suffix_is_valid(const char *zName){ | |
| 892 | + int i, j; | |
| 893 | + int uid; | |
| 894 | + i64 timestamp; | |
| 895 | + i64 now; | |
| 896 | + char *zHash; | |
| 897 | + for(i=0; fossil_isxdigit(zName[i]); i++){} | |
| 898 | + if( i<1 || zName[i]!='-' ) goto not_valid_suffix; | |
| 899 | + for(j=i+1; fossil_isxdigit(zName[j]); j++){} | |
| 900 | + if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix; | |
| 901 | + uid = strtol(zName, 0, 16); | |
| 902 | + if( uid<=0 ) goto not_valid_suffix; | |
| 903 | + if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){ | |
| 904 | + goto not_valid_suffix; | |
| 905 | + } | |
| 906 | + timestamp = strtoll(&zName[i+1], 0, 16); | |
| 907 | + now = time(0); | |
| 908 | + if( timestamp+3600 <= now ) goto not_valid_suffix; | |
| 909 | + zHash = login_resetpw_suffix(uid,timestamp); | |
| 910 | + if( fossil_strcmp(zHash, zName)!=0 ){ | |
| 911 | + fossil_free(zHash); | |
| 912 | + goto not_valid_suffix; | |
| 913 | + } | |
| 914 | + fossil_free(zHash); | |
| 915 | + return uid; | |
| 916 | + | |
| 917 | +not_valid_suffix: | |
| 918 | + sleep(2); /* Introduce a small delay on an invalid suffix as an | |
| 919 | + ** extra defense against search attacks */ | |
| 920 | + return 0; | |
| 921 | +} | |
| 922 | + | |
| 923 | +/* | |
| 924 | +** COMMAND: test-resetpw-url | |
| 925 | +** Usage: fossil test-resetpw-url UID | |
| 926 | +** | |
| 927 | +** Generate and verify a /resetpw URL for user UID. | |
| 928 | +*/ | |
| 929 | +void test_resetpw_url(void){ | |
| 930 | + char *zSuffix; | |
| 931 | + int uid; | |
| 932 | + db_find_and_open_repository(0, 0); | |
| 933 | + verify_all_options(); | |
| 934 | + if( g.argc!=3 ){ | |
| 935 | + usage("UID"); | |
| 936 | + } | |
| 937 | + uid = atoi(g.argv[2]); | |
| 938 | + zSuffix = login_resetpw_suffix(uid, 0); | |
| 939 | + fossil_print("/resetpw/%s %d\n", zSuffix, | |
| 940 | + login_resetpw_suffix_is_valid(zSuffix)); | |
| 941 | + fossil_free(zSuffix); | |
| 942 | +} | |
| 943 | + | |
| 944 | +/* | |
| 945 | +** WEBPAGE: resetpw | |
| 946 | +** | |
| 947 | +** The URL format must be like this: | |
| 948 | +** | |
| 949 | +** /resetpw/UID-TIMESTAMP-HASH | |
| 950 | +** | |
| 951 | +** Where UID is the uid of the user whose password is to be reset, | |
| 952 | +** TIMESTAMP is the unix timestamp when the request was made, and | |
| 953 | +** HASH is a hash based on UID, TIMESTAMP, and other information that | |
| 954 | +** is unavailable to an attacher. | |
| 955 | +** | |
| 956 | +** With no other arguments, a form is present which allows the user to | |
| 957 | +** enter a new password. When the SUBMIT button is pressed, a POST request | |
| 958 | +** back to the same URL that will change the password. | |
| 959 | +*/ | |
| 960 | +void login_resetpw(void){ | |
| 961 | + const char *zName; | |
| 962 | + int uid; | |
| 963 | + char *zRPW; | |
| 964 | + const char *zNew1, *zNew2; | |
| 965 | + | |
| 966 | + style_set_current_feature("resetpw"); | |
| 967 | + style_header("Reset Password"); | |
| 968 | + style_adunit_config(ADUNIT_OFF); | |
| 969 | + zName = PD("name",""); | |
| 970 | + uid = login_resetpw_suffix_is_valid(zName); | |
| 971 | + if( uid==0 ){ | |
| 972 | + @ <p><span class="loginError"> | |
| 973 | + @ This password-reset URL is invalid, probably because it has expired. | |
| 974 | + @ Password-reset URLs have a short lifespan. | |
| 975 | + @ </span></p> | |
| 976 | + style_finish_page(); | |
| 977 | + return; | |
| 978 | + } | |
| 979 | + login_set_uid(uid, 0); | |
| 980 | + if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){ | |
| 981 | + @ <p><span class="loginError"> | |
| 982 | + @ Cannot change the password for user <b>%h(g.zLogin)</b>. | |
| 983 | + @ </span></p> | |
| 984 | + style_finish_page(); | |
| 985 | + return; | |
| 986 | + } | |
| 987 | + if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ | |
| 988 | + if( fossil_strcmp(zNew1,zNew2)!=0 ){ | |
| 989 | + @ <p><span class="loginError"> | |
| 990 | + @ The two copies of your new passwords do not match. | |
| 991 | + @ Try again. | |
| 992 | + @ </span></p> | |
| 993 | + }else{ | |
| 994 | + char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); | |
| 995 | + char *zChngPw; | |
| 996 | + char *zErr; | |
| 997 | + int rc; | |
| 998 | + | |
| 999 | + /* vvvvvvv--- tag-20230106-2 ----vvvvvv | |
| 1000 | + ** | |
| 1001 | + ** Replicate changes made below to tag-20230106-1 | |
| 1002 | + */ | |
| 1003 | + db_unprotect(PROTECT_USER); | |
| 1004 | + db_multi_exec( | |
| 1005 | + "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid | |
| 1006 | + ); | |
| 1007 | + zChngPw = mprintf( | |
| 1008 | + "UPDATE user" | |
| 1009 | + " SET pw=shared_secret(%Q,%Q," | |
| 1010 | + " (SELECT value FROM config WHERE name='project-code'))" | |
| 1011 | + " WHERE login=%Q", | |
| 1012 | + zNew1, g.zLogin, g.zLogin | |
| 1013 | + ); | |
| 1014 | + fossil_free(zNewPw); | |
| 1015 | + rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr); | |
| 1016 | + db_protect_pop(); | |
| 1017 | + /* | |
| 1018 | + ** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^ | |
| 1019 | + ** | |
| 1020 | + ** Replicate changes above to tag-20230106-1 | |
| 1021 | + */ | |
| 1022 | + | |
| 1023 | + if( rc ){ | |
| 1024 | + @ <p><span class='loginError'> | |
| 1025 | + @ %s(zErr); | |
| 1026 | + @ </span></p> | |
| 1027 | + fossil_free(zErr); | |
| 1028 | + }else{ | |
| 1029 | + redirect_to_g(); | |
| 1030 | + return; | |
| 1031 | + } | |
| 1032 | + } | |
| 1033 | + } | |
| 1034 | + zRPW = fossil_random_password(12); | |
| 1035 | + @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p> | |
| 1036 | + form_begin(0, "%R/resetpw"); | |
| 1037 | + @ <input type='hidden' name='name' value='%h(zName)'> | |
| 1038 | + @ <table> | |
| 1039 | + @ <tr><td class="form_label" id="newpw">New Password:</td> | |
| 1040 | + @ <td><input aria-labelledby="newpw" type="password" name="n1" \ | |
| 1041 | + @ size="30" /> Suggestion: %z(zRPW)</td></tr> | |
| 1042 | + @ <tr><td class="form_label" id="reppw">Repeat New Password:</td> | |
| 1043 | + @ <td><input aria-labledby="reppw" type="password" name="n2" \ | |
| 1044 | + @ size="30" /></td></tr> | |
| 1045 | + @ <tr><td></td> | |
| 1046 | + @ <td><input type="submit" value="Change Password" /></td></tr> | |
| 1047 | + @ </table> | |
| 1048 | + @ </form> | |
| 1049 | + style_finish_page(); | |
| 1050 | +} | |
| 833 | 1051 | |
| 834 | 1052 | /* |
| 835 | 1053 | ** Attempt to find login credentials for user zLogin on a peer repository |
| 836 | 1054 | ** with project code zCode. Transfer those credentials to the local |
| 837 | 1055 | ** repository. |
| @@ -999,11 +1217,10 @@ | ||
| 999 | 1217 | void login_check_credentials(void){ |
| 1000 | 1218 | int uid = 0; /* User id */ |
| 1001 | 1219 | const char *zCookie; /* Text of the login cookie */ |
| 1002 | 1220 | const char *zIpAddr; /* Raw IP address of the requestor */ |
| 1003 | 1221 | const char *zCap = 0; /* Capability string */ |
| 1004 | - const char *zPublicPages = 0; /* GLOB patterns of public pages */ | |
| 1005 | 1222 | const char *zLogin = 0; /* Login user for credentials */ |
| 1006 | 1223 | |
| 1007 | 1224 | /* Only run this check once. */ |
| 1008 | 1225 | if( g.userUid!=0 ) return; |
| 1009 | 1226 | |
| @@ -1139,10 +1356,21 @@ | ||
| 1139 | 1356 | zCap = ""; |
| 1140 | 1357 | } |
| 1141 | 1358 | sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none"); |
| 1142 | 1359 | } |
| 1143 | 1360 | |
| 1361 | + login_set_uid(uid, zCap); | |
| 1362 | +} | |
| 1363 | + | |
| 1364 | +/* | |
| 1365 | +** Set the current logged in user to be uid. zCap is precomputed | |
| 1366 | +** (override) capabilities. If zCap==0, then look up the capabilities | |
| 1367 | +** in the USER table. | |
| 1368 | +*/ | |
| 1369 | +void login_set_uid(int uid, const char *zCap){ | |
| 1370 | + const char *zPublicPages = 0; /* GLOB patterns of public pages */ | |
| 1371 | + | |
| 1144 | 1372 | /* At this point, we know that uid!=0. Find the privileges associated |
| 1145 | 1373 | ** with user uid. |
| 1146 | 1374 | */ |
| 1147 | 1375 | assert( uid!=0 ); |
| 1148 | 1376 | if( zCap==0 ){ |
| 1149 | 1377 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -613,10 +613,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 +631,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(); |
| @@ -828,10 +838,218 @@ | |
| 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 +1217,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 +1356,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 ){ |
| 1149 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -613,10 +613,14 @@ | |
| 613 | char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); |
| 614 | char *zChngPw; |
| 615 | char *zErr; |
| 616 | int rc; |
| 617 | |
| 618 | /* vvvvvvv--- tag-20230106-1 ----vvvvvv |
| 619 | ** |
| 620 | ** Replicate changes made below to tag-20230106-2 |
| 621 | */ |
| 622 | db_unprotect(PROTECT_USER); |
| 623 | db_multi_exec( |
| 624 | "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 625 | ); |
| 626 | zChngPw = mprintf( |
| @@ -627,10 +631,16 @@ | |
| 631 | zNew1, g.zLogin, g.zLogin |
| 632 | ); |
| 633 | fossil_free(zNewPw); |
| 634 | rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr); |
| 635 | db_protect_pop(); |
| 636 | /* |
| 637 | ** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^ |
| 638 | ** |
| 639 | ** Replicate changes above to tag-20230106-2 |
| 640 | */ |
| 641 | |
| 642 | if( rc ){ |
| 643 | zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr); |
| 644 | fossil_free(zErr); |
| 645 | }else{ |
| 646 | redirect_to_g(); |
| @@ -828,10 +838,218 @@ | |
| 838 | @ </form> |
| 839 | } |
| 840 | } |
| 841 | style_finish_page(); |
| 842 | } |
| 843 | |
| 844 | /* |
| 845 | ** Construct an appropriate URL suffix for the /resetpw page. The |
| 846 | ** suffix will be of the form: |
| 847 | ** |
| 848 | ** UID-TIMESTAMP-HASH |
| 849 | ** |
| 850 | ** Where UID and TIMESTAMP are the parameters to this function, and HASH |
| 851 | ** is constructed from information that is unique to the user in question |
| 852 | ** and which is not publicly available. In particular, the HASH includes |
| 853 | ** the existing user password. Thus, in order to construct a URL that can |
| 854 | ** change a password, the attacker must know the current password, in which |
| 855 | ** case that do not need to construct the URL in order to take over the |
| 856 | ** account. |
| 857 | ** |
| 858 | ** Return a pointer to the resulting string in memory obtained |
| 859 | ** from fossil_malloc(). |
| 860 | */ |
| 861 | char *login_resetpw_suffix(int uid, i64 timestamp){ |
| 862 | char *zHash; |
| 863 | char *zInnerSql; |
| 864 | char *zResult; |
| 865 | extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*); |
| 866 | if( timestamp<=0 ){ timestamp = time(0); } |
| 867 | sqlite3_shathree_init(g.db, 0, 0); |
| 868 | if( db_table_exists("repository","subscriber") ){ |
| 869 | zInnerSql = mprintf( |
| 870 | "SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode" |
| 871 | " FROM user LEFT JOIN subscriber ON suname=login" |
| 872 | " WHERE uid=%d", timestamp, uid); |
| 873 | }else{ |
| 874 | zInnerSql = mprintf( |
| 875 | "SELECT %lld, login, pw, cookie, user.mtime, user.info" |
| 876 | " FROM user WHERE uid=%d", timestamp, uid); |
| 877 | } |
| 878 | zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql); |
| 879 | fossil_free(zInnerSql); |
| 880 | zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash); |
| 881 | fossil_free(zHash); |
| 882 | return zResult; |
| 883 | } |
| 884 | |
| 885 | /* |
| 886 | ** Check to see if the "name" query parameter is a valid resetpw suffix |
| 887 | ** for a user whose password we are allowed to reset. If it is, then return |
| 888 | ** the positive integer UID for that user. If the query parameter is not |
| 889 | ** valid, return 0. |
| 890 | */ |
| 891 | static int login_resetpw_suffix_is_valid(const char *zName){ |
| 892 | int i, j; |
| 893 | int uid; |
| 894 | i64 timestamp; |
| 895 | i64 now; |
| 896 | char *zHash; |
| 897 | for(i=0; fossil_isxdigit(zName[i]); i++){} |
| 898 | if( i<1 || zName[i]!='-' ) goto not_valid_suffix; |
| 899 | for(j=i+1; fossil_isxdigit(zName[j]); j++){} |
| 900 | if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix; |
| 901 | uid = strtol(zName, 0, 16); |
| 902 | if( uid<=0 ) goto not_valid_suffix; |
| 903 | if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){ |
| 904 | goto not_valid_suffix; |
| 905 | } |
| 906 | timestamp = strtoll(&zName[i+1], 0, 16); |
| 907 | now = time(0); |
| 908 | if( timestamp+3600 <= now ) goto not_valid_suffix; |
| 909 | zHash = login_resetpw_suffix(uid,timestamp); |
| 910 | if( fossil_strcmp(zHash, zName)!=0 ){ |
| 911 | fossil_free(zHash); |
| 912 | goto not_valid_suffix; |
| 913 | } |
| 914 | fossil_free(zHash); |
| 915 | return uid; |
| 916 | |
| 917 | not_valid_suffix: |
| 918 | sleep(2); /* Introduce a small delay on an invalid suffix as an |
| 919 | ** extra defense against search attacks */ |
| 920 | return 0; |
| 921 | } |
| 922 | |
| 923 | /* |
| 924 | ** COMMAND: test-resetpw-url |
| 925 | ** Usage: fossil test-resetpw-url UID |
| 926 | ** |
| 927 | ** Generate and verify a /resetpw URL for user UID. |
| 928 | */ |
| 929 | void test_resetpw_url(void){ |
| 930 | char *zSuffix; |
| 931 | int uid; |
| 932 | db_find_and_open_repository(0, 0); |
| 933 | verify_all_options(); |
| 934 | if( g.argc!=3 ){ |
| 935 | usage("UID"); |
| 936 | } |
| 937 | uid = atoi(g.argv[2]); |
| 938 | zSuffix = login_resetpw_suffix(uid, 0); |
| 939 | fossil_print("/resetpw/%s %d\n", zSuffix, |
| 940 | login_resetpw_suffix_is_valid(zSuffix)); |
| 941 | fossil_free(zSuffix); |
| 942 | } |
| 943 | |
| 944 | /* |
| 945 | ** WEBPAGE: resetpw |
| 946 | ** |
| 947 | ** The URL format must be like this: |
| 948 | ** |
| 949 | ** /resetpw/UID-TIMESTAMP-HASH |
| 950 | ** |
| 951 | ** Where UID is the uid of the user whose password is to be reset, |
| 952 | ** TIMESTAMP is the unix timestamp when the request was made, and |
| 953 | ** HASH is a hash based on UID, TIMESTAMP, and other information that |
| 954 | ** is unavailable to an attacher. |
| 955 | ** |
| 956 | ** With no other arguments, a form is present which allows the user to |
| 957 | ** enter a new password. When the SUBMIT button is pressed, a POST request |
| 958 | ** back to the same URL that will change the password. |
| 959 | */ |
| 960 | void login_resetpw(void){ |
| 961 | const char *zName; |
| 962 | int uid; |
| 963 | char *zRPW; |
| 964 | const char *zNew1, *zNew2; |
| 965 | |
| 966 | style_set_current_feature("resetpw"); |
| 967 | style_header("Reset Password"); |
| 968 | style_adunit_config(ADUNIT_OFF); |
| 969 | zName = PD("name",""); |
| 970 | uid = login_resetpw_suffix_is_valid(zName); |
| 971 | if( uid==0 ){ |
| 972 | @ <p><span class="loginError"> |
| 973 | @ This password-reset URL is invalid, probably because it has expired. |
| 974 | @ Password-reset URLs have a short lifespan. |
| 975 | @ </span></p> |
| 976 | style_finish_page(); |
| 977 | return; |
| 978 | } |
| 979 | login_set_uid(uid, 0); |
| 980 | if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){ |
| 981 | @ <p><span class="loginError"> |
| 982 | @ Cannot change the password for user <b>%h(g.zLogin)</b>. |
| 983 | @ </span></p> |
| 984 | style_finish_page(); |
| 985 | return; |
| 986 | } |
| 987 | if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){ |
| 988 | if( fossil_strcmp(zNew1,zNew2)!=0 ){ |
| 989 | @ <p><span class="loginError"> |
| 990 | @ The two copies of your new passwords do not match. |
| 991 | @ Try again. |
| 992 | @ </span></p> |
| 993 | }else{ |
| 994 | char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); |
| 995 | char *zChngPw; |
| 996 | char *zErr; |
| 997 | int rc; |
| 998 | |
| 999 | /* vvvvvvv--- tag-20230106-2 ----vvvvvv |
| 1000 | ** |
| 1001 | ** Replicate changes made below to tag-20230106-1 |
| 1002 | */ |
| 1003 | db_unprotect(PROTECT_USER); |
| 1004 | db_multi_exec( |
| 1005 | "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 1006 | ); |
| 1007 | zChngPw = mprintf( |
| 1008 | "UPDATE user" |
| 1009 | " SET pw=shared_secret(%Q,%Q," |
| 1010 | " (SELECT value FROM config WHERE name='project-code'))" |
| 1011 | " WHERE login=%Q", |
| 1012 | zNew1, g.zLogin, g.zLogin |
| 1013 | ); |
| 1014 | fossil_free(zNewPw); |
| 1015 | rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr); |
| 1016 | db_protect_pop(); |
| 1017 | /* |
| 1018 | ** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^ |
| 1019 | ** |
| 1020 | ** Replicate changes above to tag-20230106-1 |
| 1021 | */ |
| 1022 | |
| 1023 | if( rc ){ |
| 1024 | @ <p><span class='loginError'> |
| 1025 | @ %s(zErr); |
| 1026 | @ </span></p> |
| 1027 | fossil_free(zErr); |
| 1028 | }else{ |
| 1029 | redirect_to_g(); |
| 1030 | return; |
| 1031 | } |
| 1032 | } |
| 1033 | } |
| 1034 | zRPW = fossil_random_password(12); |
| 1035 | @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p> |
| 1036 | form_begin(0, "%R/resetpw"); |
| 1037 | @ <input type='hidden' name='name' value='%h(zName)'> |
| 1038 | @ <table> |
| 1039 | @ <tr><td class="form_label" id="newpw">New Password:</td> |
| 1040 | @ <td><input aria-labelledby="newpw" type="password" name="n1" \ |
| 1041 | @ size="30" /> Suggestion: %z(zRPW)</td></tr> |
| 1042 | @ <tr><td class="form_label" id="reppw">Repeat New Password:</td> |
| 1043 | @ <td><input aria-labledby="reppw" type="password" name="n2" \ |
| 1044 | @ size="30" /></td></tr> |
| 1045 | @ <tr><td></td> |
| 1046 | @ <td><input type="submit" value="Change Password" /></td></tr> |
| 1047 | @ </table> |
| 1048 | @ </form> |
| 1049 | style_finish_page(); |
| 1050 | } |
| 1051 | |
| 1052 | /* |
| 1053 | ** Attempt to find login credentials for user zLogin on a peer repository |
| 1054 | ** with project code zCode. Transfer those credentials to the local |
| 1055 | ** repository. |
| @@ -999,11 +1217,10 @@ | |
| 1217 | void login_check_credentials(void){ |
| 1218 | int uid = 0; /* User id */ |
| 1219 | const char *zCookie; /* Text of the login cookie */ |
| 1220 | const char *zIpAddr; /* Raw IP address of the requestor */ |
| 1221 | const char *zCap = 0; /* Capability string */ |
| 1222 | const char *zLogin = 0; /* Login user for credentials */ |
| 1223 | |
| 1224 | /* Only run this check once. */ |
| 1225 | if( g.userUid!=0 ) return; |
| 1226 | |
| @@ -1139,10 +1356,21 @@ | |
| 1356 | zCap = ""; |
| 1357 | } |
| 1358 | sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none"); |
| 1359 | } |
| 1360 | |
| 1361 | login_set_uid(uid, zCap); |
| 1362 | } |
| 1363 | |
| 1364 | /* |
| 1365 | ** Set the current logged in user to be uid. zCap is precomputed |
| 1366 | ** (override) capabilities. If zCap==0, then look up the capabilities |
| 1367 | ** in the USER table. |
| 1368 | */ |
| 1369 | void login_set_uid(int uid, const char *zCap){ |
| 1370 | const char *zPublicPages = 0; /* GLOB patterns of public pages */ |
| 1371 | |
| 1372 | /* At this point, we know that uid!=0. Find the privileges associated |
| 1373 | ** with user uid. |
| 1374 | */ |
| 1375 | assert( uid!=0 ); |
| 1376 | if( zCap==0 ){ |
| 1377 |