Overview
SHA1 Hash: | b4a29fac93a027384bf30d0a9acb851dae03a367 |
---|---|
Date: | 2009-08-10 02:29:14 |
User: | drh |
Comment: | Add an ascii-art captcha for anonymous login. |
Timelines: | ancestors | descendants | both | trunk |
Other Links: | files | ZIP archive | manifest |
Tags And Properties
- branch=trunk inherited from [a28c83647d]
- sym-trunk inherited from [a28c83647d]
Changes
[hide diffs]Added src/captcha.c version [0fc3d0dfef]
@@ -1,1 +1,289 @@ +/* +** Copyright (c) 2009 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the GNU General Public +** License version 2 as published by the Free Software Foundation. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** General Public License for more details. +** +** You should have received a copy of the GNU General Public +** License along with this library; if not, write to the +** Free Software Foundation, Inc., 59 Temple Place - Suite 330, +** Boston, MA 02111-1307, USA. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code to a simple text-based CAPTCHA. Though eaily +** defeated by a sophisticated attacker, this CAPTCHA does at least make +** scripting attacks more difficult. +*/ +#include <assert.h> +#include "config.h" +#include "captcha.h" + +#if INTERFACE +#define CAPTCHA 2 /* Which captcha rendering to use */ +#endif + +/* +** Convert a hex digit into a value between 0 and 15 +*/ +static int hexValue(char c){ + if( c>='0' && c<='9' ){ + return c - '0'; + }else if( c>='a' && c<='f' ){ + return c - 'a' + 10; + }else if( c>='A' && c<='F' ){ + return c - 'A' + 10; + }else{ + return 0; + } +} + +#if CAPTCHA==1 +/* +** A 4x6 pixel bitmap font for hexadecimal digits +*/ +static const unsigned int aFont1[] = { + 0x699996, + 0x262227, + 0x69124f, + 0xf16196, + 0x26af22, + 0xf8e196, + 0x68e996, + 0xf12244, + 0x696996, + 0x699716, + 0x699f99, + 0xe9e99e, + 0x698896, + 0xe9999e, + 0xf8e88f, + 0xf8e888, +}; + +/* +** Render a 32-bit unsigned integer as an 8-digit ascii-art hex number. +** Space to hold the result is obtained from malloc() and should be freed +** by the caller. +*/ +char *captcha_render(const char *zPw){ + char *z = malloc( 500 ); + int i, j, k, m; + + k = 0; + for(i=0; i<6; i++){ + for(j=0; j<8; j++){ + unsigned char v = hexValue(zPw[j]); + v = (aFont1[v] >> ((5-i)*4)) & 0xf; + for(m=8; m>=1; m = m>>1){ + if( v & m ){ + z[k++] = 'X'; + z[k++] = 'X'; + }else{ + z[k++] = ' '; + z[k++] = ' '; + } + } + z[k++] = ' '; + z[k++] = ' '; + } + z[k++] = '\n'; + } + z[k] = 0; + return z; +} +#endif /* CAPTCHA==1 */ + + +#if CAPTCHA==2 +static const char *azFont2[] = { + /* 0 */ + " __ ", + " / \\ ", + "| () |", + " \\__/ ", + + /* 1 */ + " _ ", + "/ |", + "| |", + "|_|", + + /* 2 */ + " ___ ", + "|_ )", + " / / ", + "/___|", + + /* 3 */ + " ____", + "|__ /", + " |_ \\", + "|___/", + + /* 4 */ + " _ _ ", + "| | | ", + "|_ _|", + " |_| ", + + /* 5 */ + " ___ ", + "| __|", + "|__ \\", + "|___/", + + /* 6 */ + " __ ", + " / / ", + "/ _ \\", + "\\___/", + + /* 7 */ + " ____ ", + "|__ |", + " / / ", + " /_/ ", + + /* 8 */ + " ___ ", + "( _ )", + "/ _ \\", + "\\___/", + + /* 9 */ + " ___ ", + "/ _ \\", + "\\_, /", + " /_/ ", + + /* A */ + " ", + " /\\ ", + " / \\ ", + "/_/\\_\\", + + /* B */ + " ___ ", + "| _ )", + "| _ \\", + "|___/", + + /* C */ + " ___ ", + " / __|", + "| (__ ", + " \\___|", + + /* D */ + " ___ ", + "| \\ ", + "| |) |", + "|___/ ", + + /* E */ + " ___ ", + "| __|", + "| _| ", + "|___|", + + /* F */ + " ___ ", + "| __|", + "| _| ", + "|_| ", +}; + +/* +** Render a 32-bit unsigned integer as an 8-digit ascii-art hex number. +** Space to hold the result is obtained from malloc() and should be freed +** by the caller. +*/ +char *captcha_render(const char *zPw){ + char *z = malloc( 300 ); + int i, j, k, m; + const char *zChar; + + k = 0; + for(i=0; i<4; i++){ + for(j=0; j<8; j++){ + unsigned char v = hexValue(zPw[j]); + zChar = azFont2[4*v + i]; + for(m=0; zChar[m]; m++){ + z[k++] = zChar[m]; + } + } + z[k++] = '\n'; + } + z[k] = 0; + return z; +} +#endif /* CAPTCHA==2 */ + +/* +** COMMAND: test-captcha +*/ +void test_captcha(void){ + int i; + unsigned int v; + char *z; + + for(i=2; i<g.argc; i++){ + char zHex[30]; + v = (unsigned int)atoi(g.argv[i]); + sprintf(zHex, "%x", v); + z = captcha_render(zHex); + printf("%s:\n%s", zHex, z); + free(z); + } +} + +/* +** Compute a seed value for a captcha. The seed is public and is sent +** has a hidden parameter with the page that contains the captcha. Knowledge +** of the seed is insufficient for determining the captcha without additional +** information held only on the server and never revealed. +*/ +unsigned int captcha_seed(void){ + unsigned int x; + sqlite3_randomness(sizeof(x), &x); + x &= 0x7fffffff; + return x; +} + +/* +** Translate a captcha seed value into the captcha password string. +*/ +char *captcha_decode(unsigned int seed){ + const char *zSecret; + const char *z; + Blob b; + static char zRes[20]; + zSecret = db_get("captcha-secret", 0); + if( zSecret==0 ){ + db_multi_exec( + "REPLACE INTO config(name,value)" + " VALUES('captcha-secret', lower(hex(randomblob(20))));" + ); + zSecret = db_get("captcha-secret", 0); + assert( zSecret!=0 ); + } + blob_init(&b, 0, 0); + blob_appendf(&b, "%s-%x", zSecret, seed); + sha1sum_blob(&b, &b); + z = blob_buffer(&b); + memcpy(zRes, z, 8); + zRes[8] = 0; + return zRes; +}
Modified src/login.c from [f3badebb6b] to [5c89b898ce].
@@ -79,10 +79,34 @@ fossil_redirect_home(); } } /* +** Check to see if the anonymous login is valid. If it is valid, return +** the userid of the anonymous user. +*/ +static int isValidAnonymousLogin( + const char *zUsername, /* The username. Must be "anonymous" */ + const char *zPassword /* The supplied password */ +){ + const char *zCS; /* The captcha seed value */ + const char *zPw; /* The correct password shown in the captcha */ + int uid; /* The user ID of anonymous */ + + if( zUsername==0 ) return 0; + if( zPassword==0 ) return 0; + if( strcmp(zUsername,"anonymous")!=0 ) return 0; + zCS = P("cs"); /* The "cs" parameter is the "captcha seed" */ + if( zCS==0 ) return 0; + zPw = captcha_decode((unsigned int)atoi(zCS)); + if( strcasecmp(zPw, zPassword)!=0 ) return 0; + uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'" + " AND length(pw)>0 AND length(cap)>0"); + return uid; +} + +/* ** WEBPAGE: /login ** WEBPAGE: /logout ** ** Generate the login page */ @@ -90,10 +114,11 @@ const char *zUsername, *zPasswd; const char *zNew1, *zNew2; const char *zAnonPw = 0; int anonFlag; char *zErrMsg = ""; + int uid; /* User id loged in user */ login_check_credentials(); zUsername = P("u"); zPasswd = P("p"); anonFlag = P("anon")!=0; @@ -125,12 +150,32 @@ ); redirect_to_g(); return; } } + uid = isValidAnonymousLogin(zUsername, zPasswd); + if( uid>0 ){ + char *zNow; /* Current time (julian day number) */ + const char *zIpAddr; /* IP address of requestor */ + char *zCookie; /* The login cookie */ + const char *zCookieName; /* Name of the login cookie */ + Blob b; /* Blob used during cookie construction */ + + zIpAddr = PD("REMOTE_ADDR","nil"); + zCookieName = login_cookie_name(); + zNow = db_text("0", "SELECT julianday('now')"); + blob_init(&b, zNow, -1); + blob_appendf(&b, "/%s/%s", zIpAddr, db_get("captcha-secret","")); + sha1sum_blob(&b, &b); + zCookie = sqlite3_mprintf("anon/%s/%s", zNow, blob_buffer(&b)); + blob_reset(&b); + free(zNow); + cgi_set_cookie(zCookieName, zCookie, 0, 6*3600); + redirect_to_g(); + } if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){ - int uid = db_int(0, + uid = db_int(0, "SELECT uid FROM user" " WHERE login=%Q AND pw=%Q", zUsername, zPasswd); if( uid<=0 || strcmp(zUsername,"nobody")==0 ){ sleep(1); zErrMsg = @@ -143,21 +188,17 @@ const char *zCookieName = login_cookie_name(); const char *zExpire = db_get("cookie-expire","8766"); int expires = atoi(zExpire)*3600; const char *zIpAddr = PD("REMOTE_ADDR","nil"); - if( strcmp(zUsername, "anonymous")==0 ){ - cgi_set_cookie(zCookieName, "anonymous", 0, expires); - }else{ - zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid); - cgi_set_cookie(zCookieName, zCookie, 0, expires); - db_multi_exec( - "UPDATE user SET cookie=%Q, ipaddr=%Q, " - " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d", - zCookie, zIpAddr, expires, uid - ); - } + zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid); + cgi_set_cookie(zCookieName, zCookie, 0, expires); + db_multi_exec( + "UPDATE user SET cookie=%Q, ipaddr=%Q, " + " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d", + zCookie, zIpAddr, expires, uid + ); redirect_to_g(); } } style_header("Login/Logout"); @ %s(zErrMsg) @@ -180,15 +221,10 @@ @ </tr> if( g.zLogin==0 ){ zAnonPw = db_text(0, "SELECT pw FROM user" " WHERE login='anonymous'" " AND cap!=''"); - if( zAnonPw && anonFlag ){ - @ <tr><td></td> - @ <td>The anonymous password is "<b>%h(zAnonPw)</b>".</td> - @ </tr> - } } @ <tr> @ <td></td> @ <td><input type="submit" name="in" value="Login"></td> @ </tr> @@ -201,17 +237,21 @@ } @ enter the user-id and password at the left and press the @ "Login" button. Your user name will be stored in a browser cookie. @ You must configure your web browser to accept cookies in order for @ the login to take.</p> - if( g.zLogin==0 ){ - if( zAnonPw && !anonFlag ){ - @ <p>The password for user "anonymous" is "<b>%h(zAnonPw)</b>".</p> - @ <p> </p> - }else{ - @ <p> </p><p> </p> - } + if( zAnonPw ){ + unsigned int uSeed = captcha_seed(); + char *zCaptcha = captcha_render(captcha_decode(uSeed)); + + @ <input type="hidden" name="cs" value="%u(uSeed)"> + @ <p>To login as user <b>anonymous</b> use the following + @ 8-character hexadecimal password:</p> + @ <center><table border="1" cellpadding="10"><tr><td><pre> + @ %s(zCaptcha) + @ </pre></td></tr></table></center> + free(zCaptcha); } if( g.zLogin ){ @ <br clear="both"><hr> @ <p>To log off the system (and delete your login cookie) @ press the following button:<br> @@ -278,20 +318,44 @@ /* Check the login cookie to see if it matches a known valid user. */ if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){ if( isdigit(zCookie[0]) ){ + /* Cookies of the form "uid/randomness". There must be a + ** corresponding entry in the user table. */ uid = db_int(0, "SELECT uid FROM user" " WHERE uid=%d" " AND cookie=%Q" " AND ipaddr=%Q" " AND cexpire>julianday('now')", atoi(zCookie), zCookie, zRemoteAddr ); - }else if( zCookie[0]=='a' ){ - uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"); + }else if( memcmp(zCookie,"anon/",5)==0 ){ + /* Cookies of the form "anon/TIME/HASH". The TIME must not be + ** too old and the sha1 hash of TIME+IPADDR+SECRET must match HASH. + ** SECRET is the "captcha-secret" value in the repository. + */ + double rTime; + int i; + Blob b; + rTime = atof(&zCookie[5]); + for(i=5; zCookie[i] && zCookie[i]!='/'; i++){} + blob_init(&b, &zCookie[5], i-5); + if( zCookie[i]=='/' ){ i++; } + blob_append(&b, "/", 1); + blob_appendf(&b, "%s/%s", zRemoteAddr, db_get("captcha-secret","")); + sha1sum_blob(&b, &b); + uid = db_int(0, + "SELECT uid FROM user WHERE login='anonymous'" + " AND length(cap)>0" + " AND length(pw)>0" + " AND %f+0.25>julianday('now')" + " AND %Q=%Q", + rTime, &zCookie[i], blob_buffer(&b) + ); + blob_reset(&b); } sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zCookie); } if( uid==0 ){ @@ -474,12 +538,12 @@ } /* ** Before using the results of a form, first call this routine to verify ** that ths Anti-CSRF token is present and is valid. If the Anti-CSRF token -** is missing or is incorrect, then this emits and error message and never -** returns. +** is missing or is incorrect, that indicates a cross-site scripting attach +** so emits an error message and abort. */ void login_verify_csrf_secret(void){ const char *zCsrf; /* The CSRF secret */ if( g.okCsrf ) return; if( (zCsrf = P("csrf"))!=0 && strcmp(zCsrf, g.zCsrfToken)==0 ){
Modified src/main.mk from [9bd0c8cb97] to [c4a82e07db].
@@ -18,10 +18,11 @@ $(SRCDIR)/allrepo.c \ $(SRCDIR)/bag.c \ $(SRCDIR)/blob.c \ $(SRCDIR)/branch.c \ $(SRCDIR)/browse.c \ + $(SRCDIR)/captcha.c \ $(SRCDIR)/cgi.c \ $(SRCDIR)/checkin.c \ $(SRCDIR)/checkout.c \ $(SRCDIR)/clearsign.c \ $(SRCDIR)/clone.c \ @@ -88,10 +89,11 @@ allrepo_.c \ bag_.c \ blob_.c \ branch_.c \ browse_.c \ + captcha_.c \ cgi_.c \ checkin_.c \ checkout_.c \ clearsign_.c \ clone_.c \ @@ -158,10 +160,11 @@ allrepo.o \ bag.o \ blob.o \ branch.o \ browse.o \ + captcha.o \ cgi.o \ checkin.o \ checkout.o \ clearsign.o \ clone.o \ @@ -261,16 +264,16 @@ # noop clean: rm -f *.o *_.c $(APPNAME) VERSION.h rm -f translate makeheaders mkindex page_index.h headers - rm -f add.h admin.h allrepo.h bag.h blob.h branch.h browse.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h construct.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h http.h http_socket.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h my_page.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h rstats.h schema.h setup.h sha1.h shun.h stat.h style.h sync.h tag.h tagview.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h + rm -f add.h admin.h allrepo.h bag.h blob.h branch.h browse.h captcha.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h construct.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h http.h http_socket.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h my_page.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h rstats.h schema.h setup.h sha1.h shun.h stat.h style.h sync.h tag.h tagview.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h page_index.h: $(TRANS_SRC) mkindex ./mkindex $(TRANS_SRC) >$@ headers: page_index.h makeheaders VERSION.h - ./makeheaders add_.c:add.h admin_.c:admin.h allrepo_.c:allrepo.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h construct_.c:construct.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h http_.c:http.h http_socket_.c:http_socket.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h my_page_.c:my_page.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h rstats_.c:rstats.h schema_.c:schema.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tagview_.c:tagview.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h + ./makeheaders add_.c:add.h admin_.c:admin.h allrepo_.c:allrepo.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h construct_.c:construct.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h http_.c:http.h http_socket_.c:http_socket.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h my_page_.c:my_page.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h rstats_.c:rstats.h schema_.c:schema.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tagview_.c:tagview.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h touch headers headers: Makefile Makefile: add_.c: $(SRCDIR)/add.c $(SRCDIR)/VERSION translate ./translate $(SRCDIR)/add.c | sed -f $(SRCDIR)/VERSION >add_.c @@ -319,10 +322,17 @@ browse.o: browse_.c browse.h $(SRCDIR)/config.h $(XTCC) -o browse.o -c browse_.c browse.h: headers +captcha_.c: $(SRCDIR)/captcha.c $(SRCDIR)/VERSION translate + ./translate $(SRCDIR)/captcha.c | sed -f $(SRCDIR)/VERSION >captcha_.c + +captcha.o: captcha_.c captcha.h $(SRCDIR)/config.h + $(XTCC) -o captcha.o -c captcha_.c + +captcha.h: headers cgi_.c: $(SRCDIR)/cgi.c $(SRCDIR)/VERSION translate ./translate $(SRCDIR)/cgi.c | sed -f $(SRCDIR)/VERSION >cgi_.c cgi.o: cgi_.c cgi.h $(SRCDIR)/config.h $(XTCC) -o cgi.o -c cgi_.c
Modified src/makemake.tcl from [263645557b] to [725becf5eb].
@@ -12,10 +12,11 @@ allrepo bag blob branch browse + captcha cgi checkin checkout clearsign clone