Index: src/add.c ================================================================== --- src/add.c +++ src/add.c @@ -156,10 +156,11 @@ */ static int add_one_file( const char *zPath, /* Tree-name of file to add. */ int vid /* Add to this VFILE */ ){ + int doSkip = 0; if( !file_is_simple_pathname(zPath, 1) ){ fossil_warning("filename contains illegal characters: %s", zPath); return 0; } if( db_exists("SELECT 1 FROM vfile" @@ -168,17 +169,22 @@ " WHERE pathname=%Q %s AND deleted", zPath, filename_collation()); }else{ char *zFullname = mprintf("%s%s", g.zLocalRoot, zPath); int isExe = file_isexe(zFullname, RepoFILE); - db_multi_exec( - "INSERT INTO vfile(vid,deleted,rid,mrid,pathname,isexe,islink,mhash)" - "VALUES(%d,0,0,0,%Q,%d,%d,NULL)", - vid, zPath, isExe, file_islink(0)); + if( file_nondir_objects_on_path(g.zLocalRoot, zFullname) ){ + /* Do not add unsafe files to the vfile */ + doSkip = 1; + }else{ + db_multi_exec( + "INSERT INTO vfile(vid,deleted,rid,mrid,pathname,isexe,islink,mhash)" + "VALUES(%d,0,0,0,%Q,%d,%d,NULL)", + vid, zPath, isExe, file_islink(0)); + } fossil_free(zFullname); } - if( db_changes() ){ + if( db_changes() && !doSkip ){ fossil_print("ADDED %s\n", zPath); return 1; }else{ fossil_print("SKIP %s\n", zPath); return 0; @@ -186,11 +192,13 @@ } /* ** Add all files in the sfile temp table. ** -** Automatically exclude the repository file. +** Automatically exclude the repository file and any other files +** with reserved names. Also exclude files that are beneath an +** existing symlink. */ static int add_files_in_sfile(int vid){ const char *zRepo; /* Name of the repository database file */ int nAdd = 0; /* Number of files added */ int i; /* Loop counter */ @@ -208,18 +216,30 @@ if( filenames_are_case_sensitive() ){ xCmp = fossil_strcmp; }else{ xCmp = fossil_stricmp; } - db_prepare(&loop, "SELECT pathname FROM sfile ORDER BY pathname"); + db_prepare(&loop, + "SELECT pathname FROM sfile" + " WHERE pathname NOT IN (" + "SELECT sfile.pathname FROM vfile, sfile" + " WHERE vfile.islink" + " AND NOT vfile.deleted" + " AND sfile.pathname>(vfile.pathname||'/')" + " AND sfile.pathname<(vfile.pathname||'0'))" + " ORDER BY pathname"); while( db_step(&loop)==SQLITE_ROW ){ const char *zToAdd = db_column_text(&loop, 0); if( fossil_strcmp(zToAdd, zRepo)==0 ) continue; - for(i=0; (zReserved = fossil_reserved_name(i, 0))!=0; i++){ - if( xCmp(zToAdd, zReserved)==0 ) break; + if( strchr(zToAdd,'/') ){ + if( file_is_reserved_name(zToAdd, -1) ) continue; + }else{ + for(i=0; (zReserved = fossil_reserved_name(i, 0))!=0; i++){ + if( xCmp(zToAdd, zReserved)==0 ) break; + } + if( zReserved ) continue; } - if( zReserved ) continue; nAdd += add_one_file(zToAdd, vid); } db_finalize(&loop); blob_reset(&repoName); return nAdd; Index: src/alerts.c ================================================================== --- src/alerts.c +++ src/alerts.c @@ -936,11 +936,11 @@ ** This is a short name used to identifies the repository in the Subject: ** line of email alerts. Traditionally this name is included in square ** brackets. Examples: "[fossil-src]", "[sqlite-src]". */ /* -** SETTING: email-send-method width=5 default=off +** SETTING: email-send-method width=5 default=off sensitive ** Determine the method used to send email. Allowed values are ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value ** means no email is ever sent. The "relay" value means emails are sent ** to an Mail Sending Agent using SMTP located at email-send-relayhost. ** The "pipe" value means email messages are piped into a command @@ -949,33 +949,33 @@ ** by the email-send-dir setting. The "db" value means that emails ** are added to an SQLite database named by the* email-send-db setting. ** The "stdout" value writes email text to standard output, for debugging. */ /* -** SETTING: email-send-command width=40 +** SETTING: email-send-command width=40 sensitive ** This is a command to which outbound email content is piped when the ** email-send-method is set to "pipe". The command must extract ** recipient, sender, subject, and all other relevant information ** from the email header. */ /* -** SETTING: email-send-dir width=40 +** SETTING: email-send-dir width=40 sensitive ** This is a directory into which outbound emails are written as individual ** files if the email-send-method is set to "dir". */ /* -** SETTING: email-send-db width=40 +** SETTING: email-send-db width=40 sensitive ** This is an SQLite database file into which outbound emails are written ** if the email-send-method is set to "db". */ /* ** SETTING: email-self width=40 ** This is the email address for the repository. Outbound emails add ** this email address as the "From:" field. */ /* -** SETTING: email-send-relayhost width=40 +** SETTING: email-send-relayhost width=40 sensitive ** This is the hostname and TCP port to which output email messages ** are sent when email-send-method is "relay". There should be an ** SMTP server configured as a Mail Submission Agent listening on the ** designated host and port and all times. */ @@ -1769,18 +1769,20 @@ "UPDATE subscriber SET sverified=1" " WHERE subscriberCode=hextoblob(%Q)", zName); if( db_get_boolean("selfreg-verify",0) ){ char *zNewCap = db_get("default-perms","u"); + db_unprotect(PROTECT_USER); db_multi_exec( "UPDATE user" " SET cap=%Q" " WHERE cap='7' AND login=(" " SELECT suname FROM subscriber" " WHERE subscriberCode=hextoblob(%Q))", zNewCap, zName ); + db_protect_pop(); login_set_capabilities(zNewCap, 0); } @

Your email alert subscription has been verified!

@

Use the form below to update your subscription information.

@

Hint: Bookmark this page so that you can more easily update Index: src/allrepo.c ================================================================== --- src/allrepo.c +++ src/allrepo.c @@ -299,11 +299,13 @@ useCheckouts?"ckout":"repo", blob_str(&fn) ); if( dryRunFlag ){ fossil_print("%s\n", blob_sql_text(&sql)); }else{ + db_unprotect(PROTECT_CONFIG); db_multi_exec("%s", blob_sql_text(&sql)); + db_protect_pop(); } } db_end_transaction(0); blob_reset(&sql); blob_reset(&fn); @@ -334,11 +336,13 @@ "VALUES('repo:%q',1)", z ); if( dryRunFlag ){ fossil_print("%s\n", blob_sql_text(&sql)); }else{ + db_unprotect(PROTECT_CONFIG); db_multi_exec("%s", blob_sql_text(&sql)); + db_protect_pop(); } } db_end_transaction(0); blob_reset(&sql); blob_reset(&fn); @@ -428,9 +432,11 @@ if( nToDel>0 ){ const char *zSql = "DELETE FROM global_config WHERE name IN toDel"; if( dryRunFlag ){ fossil_print("%s\n", zSql); }else{ + db_unprotect(PROTECT_CONFIG); db_multi_exec("%s", zSql /*safe-for-%s*/ ); + db_protect_pop(); } } } Index: src/backoffice.c ================================================================== --- src/backoffice.c +++ src/backoffice.c @@ -241,10 +241,11 @@ ** process (1) no longer exists and the current time exceeds (2). */ static void backofficeReadLease(Lease *pLease){ Stmt q; memset(pLease, 0, sizeof(*pLease)); + db_unprotect(PROTECT_CONFIG); db_prepare(&q, "SELECT value FROM repository.config" " WHERE name='backoffice'"); if( db_step(&q)==SQLITE_ROW ){ const char *z = db_column_text(&q,0); z = backofficeParseInt(z, &pLease->idCurrent); @@ -251,10 +252,11 @@ z = backofficeParseInt(z, &pLease->tmCurrent); z = backofficeParseInt(z, &pLease->idNext); backofficeParseInt(z, &pLease->tmNext); } db_finalize(&q); + db_protect_pop(); } /* ** Return a string that describes how long it has been since the ** last backoffice run. The string is obtained from fossil_malloc(). @@ -277,15 +279,17 @@ /* ** Write a lease to the backoffice property */ static void backofficeWriteLease(Lease *pLease){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO repository.config(name,value,mtime)" " VALUES('backoffice','%lld %lld %lld %lld',now())", pLease->idCurrent, pLease->tmCurrent, pLease->idNext, pLease->tmNext); + db_protect_pop(); } /* ** Check to see if the specified Win32 process is still alive. It ** should be noted that even if this function returns non-zero, the Index: src/captcha.c ================================================================== --- src/captcha.c +++ src/captcha.c @@ -458,14 +458,16 @@ Blob b; static char zRes[20]; zSecret = db_get("captcha-secret", 0); if( zSecret==0 ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name,value)" " VALUES('captcha-secret', lower(hex(randomblob(20))));" ); + db_protect_pop(); zSecret = db_get("captcha-secret", 0); assert( zSecret!=0 ); } blob_init(&b, 0, 0); blob_appendf(&b, "%s-%x", zSecret, seed); Index: src/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -62,10 +62,13 @@ ** Create a TEMP table named SFILE and add all unmanaged files named on ** the command-line to that table. If directories are named, then add ** all unmanaged files contained underneath those directories. If there ** are no files or directories named on the command-line, then add all ** unmanaged files anywhere in the checkout. +** +** This routine never follows symlinks. It always treats symlinks as +** object unto themselves. */ static void locate_unmanaged_files( int argc, /* Number of command-line arguments to examine */ char **argv, /* values of command-line arguments */ unsigned scanFlags, /* Zero or more SCAN_xxx flags */ @@ -80,19 +83,19 @@ db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s," " mtime INTEGER, size INTEGER)", filename_collation()); nRoot = (int)strlen(g.zLocalRoot); if( argc==0 ){ blob_init(&name, g.zLocalRoot, nRoot - 1); - vfile_scan(&name, blob_size(&name), scanFlags, pIgnore, 0, RepoFILE); + vfile_scan(&name, blob_size(&name), scanFlags, pIgnore, 0, SymFILE); blob_reset(&name); }else{ for(i=0; i1000 && db_int(0, "PRAGMA page_size")<8192 ){ db_multi_exec("PRAGMA page_size=8192;"); } + db_unprotect(PROTECT_ALL); db_multi_exec("VACUUM"); + db_protect_pop(); fossil_print("\nproject-id: %s\n", db_get("project-code", 0)); fossil_print("server-id: %s\n", db_get("server-code", 0)); zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); fossil_print("admin-user: %s (password is \"%s\")\n", g.zLogin, zPassword); } Index: src/configure.c ================================================================== --- src/configure.c +++ src/configure.c @@ -145,11 +145,10 @@ { "keep-glob", CONFIGSET_PROJ }, { "crlf-glob", CONFIGSET_PROJ }, { "crnl-glob", CONFIGSET_PROJ }, { "encoding-glob", CONFIGSET_PROJ }, { "empty-dirs", CONFIGSET_PROJ }, - { "allow-symlinks", CONFIGSET_PROJ }, { "dotfiles", CONFIGSET_PROJ }, { "parent-project-code", CONFIGSET_PROJ }, { "parent-project-name", CONFIGSET_PROJ }, { "hash-policy", CONFIGSET_PROJ }, { "comment-format", CONFIGSET_PROJ }, @@ -446,10 +445,11 @@ blob_append_sql(&sql,") VALUES(%s,%s", azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/); for(jj=2; jj=count(db.aProtect)-2 ){ + fossil_panic("too many db_protect() calls"); + } + db.aProtect[db.nProtect++] = db.protectMask; + if( (flags & PROTECT_SENSITIVE)!=0 + && db.bProtectTriggers==0 + && g.repositoryOpen + ){ + /* Create the triggers needed to protect sensitive settings from + ** being created or modified the first time that PROTECT_SENSITIVE + ** is enabled. Deleting a sensitive setting is harmless, so there + ** is not trigger to block deletes. After being created once, the + ** triggers persist for the life of the database connection. */ + db_multi_exec( + "CREATE TEMP TRIGGER protect_1 BEFORE INSERT ON config" + " WHEN protected_setting(new.name) BEGIN" + " SELECT raise(abort,'not authorized');" + "END;\n" + "CREATE TEMP TRIGGER protect_2 BEFORE UPDATE ON config" + " WHEN protected_setting(new.name) BEGIN" + " SELECT raise(abort,'not authorized');" + "END;\n" + ); + db.bProtectTriggers = 1; + } + db.protectMask = flags; +} +void db_protect(unsigned flags){ + db_protect_only(db.protectMask | flags); +} +void db_unprotect(unsigned flags){ + if( db.nProtect>=count(db.aProtect)-2 ){ + fossil_panic("too many db_unprotect() calls"); + } + db.aProtect[db.nProtect++] = db.protectMask; + db.protectMask &= ~flags; +} +void db_protect_pop(void){ + if( db.nProtect<1 ){ + fossil_panic("too many db_protect_pop() calls"); + } + db.protectMask = db.aProtect[--db.nProtect]; +} + +/* +** Verify that the desired database write pertections are in place. +** Throw a fatal error if not. +*/ +void db_assert_protected(unsigned flags){ + if( (flags & db.protectMask)!=flags ){ + fossil_panic("missing database write protection bits: %02x", + flags & ~db.protectMask); + } +} + +/* +** Assert that either all protections are off (including PROTECT_BASELINE +** which is usually always enabled), or the setting named in the argument +** is no a sensitive setting. +** +** This assert() is used to verify that the db_set() and db_set_int() +** interfaces do not modify a sensitive setting. +*/ +void db_assert_protection_off_or_not_sensitive(const char *zName){ + if( db.protectMask!=0 && db_setting_is_protected(zName) ){ + fossil_panic("unauthorized change to protected setting \"%s\"", zName); + } +} + +/* +** Every Fossil database connection automatically registers the following +** overarching authenticator callback, and leaves it registered for the +** duration of the connection. This authenticator will call any +** sub-authenticators that are registered using db_set_authorizer(). +*/ +int db_top_authorizer( + void *pNotUsed, + int eCode, + const char *z0, + const char *z1, + const char *z2, + const char *z3 +){ + int rc = SQLITE_OK; + switch( eCode ){ + case SQLITE_INSERT: + case SQLITE_UPDATE: + case SQLITE_DELETE: { + if( (db.protectMask & PROTECT_USER)!=0 + && sqlite3_stricmp(z0,"user")==0 ){ + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_CONFIG)!=0 && + (sqlite3_stricmp(z0,"config")==0 || + sqlite3_stricmp(z0,"global_config")==0) ){ + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_SENSITIVE)!=0 && + sqlite3_stricmp(z0,"global_config")==0 ){ + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_READONLY)!=0 + && sqlite3_stricmp(z2,"temp")!=0 ){ + rc = SQLITE_DENY; + } + break; + } + case SQLITE_DROP_TEMP_TRIGGER: { + /* Do not allow the triggers that enforce PROTECT_SENSITIVE + ** to be dropped */ + rc = SQLITE_DENY; + break; + } + } + if( db.xAuth && rc==SQLITE_OK ){ + rc = db.xAuth(db.pAuthArg, eCode, z0, z1, z2, z3); + } + return rc; +} + +/* +** Set or unset the query authorizer callback function +*/ +void db_set_authorizer( + int(*xAuth)(void*,int,const char*,const char*,const char*,const char*), + void *pArg, + const char *zName /* for tracing */ +){ + if( db.xAuth ){ + fossil_panic("multiple active db_set_authorizer() calls"); + } + db.xAuth = xAuth; + db.pAuthArg = pArg; + db.zAuthName = zName; + if( g.fSqlTrace ) fossil_trace("-- set authorizer %s\n", zName); +} +void db_clear_authorizer(void){ + if( db.zAuthName && g.fSqlTrace ){ + fossil_trace("-- discontinue authorizer %s\n", db.zAuthName); + } + db.xAuth = 0; + db.pAuthArg = 0; + db.zAuthName = 0; +} #if INTERFACE /* ** Possible flags to db_vprepare */ @@ -334,21 +566,24 @@ */ int db_vprepare(Stmt *pStmt, int flags, const char *zFormat, va_list ap){ int rc; int prepFlags = 0; char *zSql; + const char *zExtra = 0; blob_zero(&pStmt->sql); blob_vappendf(&pStmt->sql, zFormat, ap); va_end(ap); zSql = blob_str(&pStmt->sql); db.nPrepare++; if( flags & DB_PREPARE_PERSISTENT ){ prepFlags = SQLITE_PREPARE_PERSISTENT; } - rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, 0); + rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, &zExtra); if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)==0 ){ db_err("%s\n%s", sqlite3_errmsg(g.db), zSql); + }else if( zExtra && !fossil_all_whitespace(zExtra) ){ + db_err("surplus text follows SQL: \"%s\"", zExtra); } pStmt->pNext = db.pAllStmt; pStmt->pPrev = 0; if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt; db.pAllStmt = pStmt; @@ -611,10 +846,11 @@ return rc; } /* ** COMMAND: test-db-exec-error +** Usage: %fossil test-db-exec-error ** ** Invoke the db_exec() interface with an erroneous SQL statement ** in order to verify the error handling logic. */ void db_test_db_exec_cmd(void){ @@ -621,10 +857,27 @@ Stmt err; db_find_and_open_repository(0,0); db_prepare(&err, "INSERT INTO repository.config(name) VALUES(NULL);"); db_exec(&err); } + +/* +** COMMAND: test-db-prepare +** Usage: %fossil test-db-prepare ?OPTIONS? SQL +** +** Invoke db_prepare() on the SQL input. Report any errors encountered. +** This command is used to verify error detection logic in the db_prepare() +** utility routine. +*/ +void db_test_db_prepare(void){ + Stmt err; + db_find_and_open_repository(0,0); + verify_all_options(); + if( g.argc!=3 ) usage("?OPTIONS? SQL"); + db_prepare(&err, "%s", g.argv[2]/*safe-for-%s*/); + db_finalize(&err); +} /* ** Print the output of one or more SQL queries on standard output. ** This routine is used for debugging purposes only. */ @@ -844,34 +1097,34 @@ void db_init_database( const char *zFileName, /* Name of database file to create */ const char *zSchema, /* First part of schema */ ... /* Additional SQL to run. Terminate with NULL. */ ){ - sqlite3 *db; + sqlite3 *xdb; int rc; const char *zSql; va_list ap; - db = db_open(zFileName ? zFileName : ":memory:"); - sqlite3_exec(db, "BEGIN EXCLUSIVE", 0, 0, 0); - rc = sqlite3_exec(db, zSchema, 0, 0, 0); + xdb = db_open(zFileName ? zFileName : ":memory:"); + sqlite3_exec(xdb, "BEGIN EXCLUSIVE", 0, 0, 0); + rc = sqlite3_exec(xdb, zSchema, 0, 0, 0); if( rc!=SQLITE_OK ){ - db_err("%s", sqlite3_errmsg(db)); + db_err("%s", sqlite3_errmsg(xdb)); } va_start(ap, zSchema); while( (zSql = va_arg(ap, const char*))!=0 ){ - rc = sqlite3_exec(db, zSql, 0, 0, 0); + rc = sqlite3_exec(xdb, zSql, 0, 0, 0); if( rc!=SQLITE_OK ){ - db_err("%s", sqlite3_errmsg(db)); + db_err("%s", sqlite3_errmsg(xdb)); } } va_end(ap); - sqlite3_exec(db, "COMMIT", 0, 0, 0); + sqlite3_exec(xdb, "COMMIT", 0, 0, 0); if( zFileName || g.db!=0 ){ - sqlite3_close(db); + sqlite3_close(xdb); }else{ - g.db = db; + g.db = xdb; } } /* ** Function to return the number of seconds since 1970. This is @@ -1060,10 +1313,37 @@ } strcpy(zOut, zTemp = obscure((char*)zIn)); fossil_free(zTemp); sqlite3_result_text(context, zOut, strlen(zOut), sqlite3_free); } + +/* +** Return True if zName is a protected (a.k.a. "sensitive") setting. +*/ +int db_setting_is_protected(const char *zName){ + const Setting *pSetting = zName ? db_find_setting(zName,0) : 0; + return pSetting!=0 && pSetting->sensitive!=0; +} + +/* +** Implement the protected_setting(X) SQL function. This function returns +** true if X is the name of a protected (security-sensitive) setting and +** the db.protectSensitive flag is enabled. It returns false otherwise. +*/ +LOCAL void db_protected_setting_func( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *zSetting; + if( (db.protectMask & PROTECT_SENSITIVE)==0 ){ + sqlite3_result_int(context, 0); + return; + } + zSetting = (const char*)sqlite3_value_text(argv[0]); + sqlite3_result_int(context, db_setting_is_protected(zSetting)); +} /* ** Register the SQL functions that are useful both to the internal ** representation and to the "fossil sql" command. */ @@ -1090,10 +1370,12 @@ alert_find_emailaddr_func, 0, 0); sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, alert_display_name_func, 0, 0); sqlite3_create_function(db, "obscure", 1, SQLITE_UTF8, 0, db_obscure, 0, 0); + sqlite3_create_function(db, "protected_setting", 1, SQLITE_UTF8, 0, + db_protected_setting_func, 0, 0); } #if USE_SEE /* ** This is a pointer to the saved database encryption key string. @@ -1348,10 +1630,11 @@ if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); db_add_aux_functions(db); re_add_sql_func(db); /* The REGEXP operator */ foci_register(db); /* The "files_of_checkin" virtual table */ sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 0, &rc); + sqlite3_set_authorizer(db, db_top_authorizer, db); return db; } /* @@ -1791,22 +2074,10 @@ } } return zRepo; } -/* -** Returns non-zero if the default value for the "allow-symlinks" setting -** is "on". When on Windows, this always returns false. -*/ -int db_allow_symlinks_by_default(void){ -#if defined(_WIN32) - return 0; -#else - return 1; -#endif -} - /* ** Returns non-zero if support for symlinks is currently enabled. */ int db_allow_symlinks(void){ return g.allowSymlinks; @@ -1848,13 +2119,14 @@ g.zRepositoryName = mprintf("%s", zDbName); db_open_or_attach(g.zRepositoryName, "repository"); g.repositoryOpen = 1; sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION, &g.iRepoDataVers); + /* Cache "allow-symlinks" option, because we'll need it on every stat call */ - g.allowSymlinks = db_get_boolean("allow-symlinks", - db_allow_symlinks_by_default()); + g.allowSymlinks = db_get_boolean("allow-symlinks",0); + g.zAuxSchema = db_get("aux-schema",""); g.eHashPolicy = db_get_int("hash-policy",-1); if( g.eHashPolicy<0 ){ g.eHashPolicy = hname_default_policy(); db_set_int("hash-policy", g.eHashPolicy, 0); @@ -2089,10 +2361,11 @@ ** argument is true. Ignore unfinalized statements when false. */ void db_close(int reportErrors){ sqlite3_stmt *pStmt; if( g.db==0 ) return; + sqlite3_set_authorizer(g.db, 0, 0); if( g.fSqlStats ){ int cur, hiwtr; sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &hiwtr, 0); fprintf(stderr, "-- LOOKASIDE_USED %10d %10d\n", cur, hiwtr); sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_HIT, &cur, &hiwtr, 0); @@ -2118,17 +2391,20 @@ fprintf(stderr, "-- prepared statements %10d\n", db.nPrepare); } while( db.pAllStmt ){ db_finalize(db.pAllStmt); } - if( db.nBegin && reportErrors ){ - fossil_warning("Transaction started at %s:%d never commits", - db.zStartFile, db.iStartLine); + if( db.nBegin ){ + if( reportErrors ){ + fossil_warning("Transaction started at %s:%d never commits", + db.zStartFile, db.iStartLine); + } db_end_transaction(1); } pStmt = 0; - g.dbIgnoreErrors++; /* Stop "database locked" warnings from PRAGMA optimize */ + sqlite3_busy_timeout(g.db, 0); + g.dbIgnoreErrors++; /* Stop "database locked" warnings */ sqlite3_exec(g.db, "PRAGMA optimize", 0, 0, 0); g.dbIgnoreErrors--; db_close_config(); /* If the localdb has a lot of unused free space, @@ -2136,11 +2412,13 @@ */ if( db_database_slot("localdb")>=0 ){ int nFree = db_int(0, "PRAGMA localdb.freelist_count"); int nTotal = db_int(0, "PRAGMA localdb.page_count"); if( nFree>nTotal/4 ){ + db_unprotect(PROTECT_ALL); db_multi_exec("VACUUM localdb;"); + db_protect_pop(); } } if( g.db ){ int rc; @@ -2154,10 +2432,11 @@ } g.db = 0; } g.repositoryOpen = 0; g.localOpen = 0; + db.bProtectTriggers = 0; assert( g.dbConfig==0 ); assert( g.zConfigDbName==0 ); backoffice_run_if_needed(); } @@ -2168,10 +2447,11 @@ if( g.db ){ int rc; sqlite3_wal_checkpoint(g.db, 0); rc = sqlite3_close(g.db); if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc); + db_clear_authorizer(); } g.db = 0; g.repositoryOpen = 0; g.localOpen = 0; } @@ -2215,10 +2495,11 @@ zUser = fossil_getenv("USERNAME"); } if( zUser==0 ){ zUser = "root"; } + db_unprotect(PROTECT_USER); db_multi_exec( "INSERT OR IGNORE INTO user(login, info) VALUES(%Q,'')", zUser ); db_multi_exec( "UPDATE user SET cap='s', pw=%Q" @@ -2234,10 +2515,11 @@ " VALUES('developer','','ei','Dev');" "INSERT OR IGNORE INTO user(login,pw,cap,info)" " VALUES('reader','','kptw','Reader');" ); } + db_protect_pop(); } /* ** Return a pointer to a string that contains the RHS of an IN operator ** that will select CONFIG table names that are in the list of control @@ -2285,10 +2567,11 @@ ){ char *zDate; Blob hash; Blob manifest; + db_unprotect(PROTECT_ALL); db_set("content-schema", CONTENT_SCHEMA, 0); db_set("aux-schema", AUX_SCHEMA_MAX, 0); db_set("rebuilt", get_version(), 0); db_set("admin-log", "1", 0); db_set("access-log", "1", 0); @@ -2343,10 +2626,11 @@ " photo = (SELECT u2.photo FROM settingSrc.user u2" " WHERE u2.login = user.login)" " WHERE user.login IN ('anonymous','nobody','developer','reader');" ); } + db_protect_pop(); if( zInitialDate ){ int rid; blob_zero(&manifest); blob_appendf(&manifest, "C initial\\sempty\\scheck-in\n"); @@ -2839,10 +3123,12 @@ z = db_text(0, "SELECT strftime(%Q,%Q,'unixepoch');", zFormat, z); } return z; } void db_set(const char *zName, const char *zValue, int globalFlag){ + db_assert_protection_off_or_not_sensitive(zName); + db_unprotect(PROTECT_CONFIG); db_begin_transaction(); if( globalFlag ){ db_swap_connections(); db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)", zName, zValue); @@ -2853,13 +3139,15 @@ } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } db_end_transaction(0); + db_protect_pop(); } void db_unset(const char *zName, int globalFlag){ db_begin_transaction(); + db_unprotect(PROTECT_CONFIG); if( globalFlag ){ db_swap_connections(); db_multi_exec("DELETE FROM global_config WHERE name=%Q", zName); db_swap_connections(); }else{ @@ -2866,10 +3154,11 @@ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } + db_protect_pop(); db_end_transaction(0); } int db_is_global(const char *zName){ int rc = 0; if( g.zConfigDbName ){ @@ -2899,10 +3188,12 @@ db_swap_connections(); } return v; } void db_set_int(const char *zName, int value, int globalFlag){ + db_assert_protection_off_or_not_sensitive(zName); + db_unprotect(PROTECT_CONFIG); if( globalFlag ){ db_swap_connections(); db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)", zName, value); db_swap_connections(); @@ -2911,10 +3202,11 @@ zName, value); } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } + db_protect_pop(); } int db_get_boolean(const char *zName, int dflt){ char *zVal = db_get(zName, dflt ? "on" : "off"); if( is_truth(zVal) ){ dflt = 1; @@ -3040,24 +3332,28 @@ } file_canonical_name(zName, &full, 0); (void)filename_collation(); /* Initialize before connection swap */ db_swap_connections(); zRepoSetting = mprintf("repo:%q", blob_str(&full)); + + db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM global_config WHERE name %s = %Q;", filename_collation(), zRepoSetting ); db_multi_exec( "INSERT OR IGNORE INTO global_config(name,value)" "VALUES(%Q,1);", zRepoSetting ); + db_protect_pop(); fossil_free(zRepoSetting); if( g.localOpen && g.zLocalRoot && g.zLocalRoot[0] ){ Blob localRoot; file_canonical_name(g.zLocalRoot, &localRoot, 1); zCkoutSetting = mprintf("ckout:%q", blob_str(&localRoot)); + db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM global_config WHERE name %s = %Q;", filename_collation(), zCkoutSetting ); db_multi_exec( @@ -3073,10 +3369,11 @@ db_optional_sql("repository", "REPLACE INTO config(name,value,mtime)" "VALUES(%Q,1,now());", zCkoutSetting ); + db_protect_pop(); fossil_free(zCkoutSetting); blob_reset(&localRoot); }else{ db_swap_connections(); } @@ -3131,11 +3428,10 @@ void cmd_open(void){ int emptyFlag; int keepFlag; int forceMissingFlag; int allowNested; - int allowSymlinks; int setmtimeFlag; /* --setmtime. Set mtimes on files */ int bForce = 0; /* --force. Open even if non-empty dir */ static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 }; const char *zWorkDir; /* --workdir value */ const char *zRepo = 0; /* Name of the repository file */ @@ -3242,23 +3538,10 @@ }else if( db_exists("SELECT 1 FROM event WHERE type='ci'") ){ g.zOpenRevision = db_get("main-branch", 0); } } - if( g.zOpenRevision ){ - /* Since the repository is open and we know the revision now, - ** refresh the allow-symlinks flag. Since neither the local - ** checkout nor the configuration database are open at this - ** point, this should always return the versioned setting, - ** if any, or the default value, which is negative one. The - ** value negative one, in this context, means that the code - ** below should fallback to using the setting value from the - ** repository or global configuration databases only. */ - allowSymlinks = db_get_versioned_boolean("allow-symlinks", -1); - }else{ - allowSymlinks = -1; /* Use non-versioned settings only. */ - } #if defined(_WIN32) || defined(__CYGWIN__) # define LOCALDB_NAME "./_FOSSIL_" #else # define LOCALDB_NAME "./.fslckout" @@ -3268,26 +3551,10 @@ "COMMIT; PRAGMA journal_mode=WAL; BEGIN;", #endif (char*)0); db_delete_on_failure(LOCALDB_NAME); db_open_local(0); - if( allowSymlinks>=0 ){ - /* Use the value from the versioned setting, which was read - ** prior to opening the local checkout (i.e. which is most - ** likely empty and does not actually contain any versioned - ** setting files yet). Normally, this value would be given - ** first priority within db_get_boolean(); however, this is - ** a special case because we know the on-disk files may not - ** exist yet. */ - g.allowSymlinks = allowSymlinks; - }else{ - /* Since the local checkout may not have any files at this - ** point, this will probably be the setting value from the - ** repository or global configuration databases. */ - g.allowSymlinks = db_get_boolean("allow-symlinks", - db_allow_symlinks_by_default()); - } db_lset("repository", zRepo); db_record_repository_filename(zRepo); db_set_checkout(0); azNewArgv[0] = g.argv[0]; g.argv = azNewArgv; @@ -3376,12 +3643,13 @@ const char *name; /* Name of the setting */ const char *var; /* Internal variable name used by db_set() */ int width; /* Width of display. 0 for boolean values and ** negative for values which should not appear ** on the /setup_settings page. */ - int versionable; /* Is this setting versionable? */ - int forceTextArea; /* Force using a text area for display? */ + char versionable; /* Is this setting versionable? */ + char forceTextArea; /* Force using a text area for display? */ + char sensitive; /* True if this a security-sensitive setting */ const char *def; /* Default value */ }; #endif /* INTERFACE */ /* @@ -3395,32 +3663,29 @@ ** SETTING: admin-log boolean default=off ** ** When the admin-log setting is enabled, configuration changes are recorded ** in the "admin_log" table of the repository. */ -#if defined(_WIN32) -/* -** SETTING: allow-symlinks boolean default=off versionable -** -** When allow-symlinks is OFF, symbolic links in the repository are followed -** and treated no differently from real files. When allow-symlinks is ON, -** the object to which the symbolic link points is ignored, and the content -** of the symbolic link that is stored in the repository is the name of the -** object to which the symbolic link points. -*/ -#endif -#if !defined(_WIN32) -/* -** SETTING: allow-symlinks boolean default=on versionable -** -** When allow-symlinks is OFF, symbolic links in the repository are followed -** and treated no differently from real files. When allow-symlinks is ON, -** the object to which the symbolic link points is ignored, and the content -** of the symbolic link that is stored in the repository is the name of the -** object to which the symbolic link points. -*/ -#endif +/* +** SETTING: allow-symlinks boolean default=off sensitive +** +** When allow-symlinks is OFF, Fossil does not see symbolic links +** (a.k.a "symlinks") on disk as a separate class of object. Instead Fossil +** sees the object that the symlink points to. Fossil will only manage files +** and directories, not symlinks. When a symlink is added to a repository, +** the object that the symlink points to is added, not the symlink itself. +** +** When allow-symlinks is ON, Fossil sees symlinks on disk as a separate +** object class that is distinct from files and directories. When a symlink +** is added to a repository, Fossil stores the target filename. In other +** words, Fossil stores the symlink itself, not the object that the symlink +** points to. +** +** Symlinks are not cross-platform. They are not available on all +** operating systems and file systems. Hence the allow-symlinks setting is +** OFF by default, for portability. +*/ /* ** SETTING: auto-captcha boolean default=on variable=autocaptcha ** If enabled, the /login page provides a button that will automatically ** fill in the captcha password. This makes things easier for human users, ** at the expense of also making logins easier for malicious robots. @@ -3470,11 +3735,11 @@ ** there is no cron job periodically running "fossil backoffice", ** email notifications and other work normally done by the ** backoffice will not occur. */ /* -** SETTING: backoffice-logfile width=40 +** SETTING: backoffice-logfile width=40 sensitive ** If backoffice-logfile is not an empty string and is a valid ** filename, then a one-line message is appended to that file ** every time the backoffice runs. This can be used for debugging, ** to ensure that backoffice is running appropriately. */ @@ -3547,11 +3812,11 @@ /* ** SETTING: crnl-glob width=40 versionable block-text ** This is an alias for the crlf-glob setting. */ /* -** SETTING: default-perms width=16 default=u +** SETTING: default-perms width=16 default=u sensitive ** Permissions given automatically to new users. For more ** information on permissions see the Users page in Server ** Administration of the HTTP UI. */ /* @@ -3559,11 +3824,11 @@ ** If enabled, permit files that may be binary ** or that match the "binary-glob" setting to be used with ** external diff programs. If disabled, skip these files. */ /* -** SETTING: diff-command width=40 +** SETTING: diff-command width=40 sensitive ** The value is an external command to run when performing a diff. ** If undefined, the internal text diff will be used. */ /* ** SETTING: dont-push boolean default=off @@ -3574,11 +3839,11 @@ /* ** SETTING: dotfiles boolean versionable default=off ** If enabled, include --dotfiles option for all compatible commands. */ /* -** SETTING: editor width=32 +** SETTING: editor width=32 sensitive ** The value is an external command that will launch the ** text editor command used for check-in comments. */ /* ** SETTING: empty-dirs width=40 versionable block-text @@ -3617,16 +3882,16 @@ ** An empty list prohibits editing via that page. Note that ** it cannot edit binary files, so the list should not ** contain any globs for, e.g., images or PDFs. */ /* -** SETTING: gdiff-command width=40 default=gdiff +** SETTING: gdiff-command width=40 default=gdiff sensitive ** The value is an external command to run when performing a graphical ** diff. If undefined, text diff will be used. */ /* -** SETTING: gmerge-command width=40 +** SETTING: gmerge-command width=40 sensitive ** The value is a graphical merge conflict resolver command operating ** on four files. Examples: ** ** kdiff3 "%baseline" "%original" "%merge" -o "%output" ** xxdiff "%original" "%baseline" "%merge" -M "%output" @@ -3757,11 +4022,11 @@ ** the associated files within the checkout -AND- the "rm" ** and "delete" commands will also remove the associated ** files from within the checkout. */ /* -** SETTING: pgp-command width=40 +** SETTING: pgp-command width=40 sensitive ** Command used to clear-sign manifests at check-in. ** Default value is "gpg --clearsign -o" */ /* ** SETTING: forbid-delta-manifests boolean default=off @@ -3817,22 +4082,22 @@ ** ** If repolist-skin has a value of 2, then the repository is omitted from ** the list in use cases 1 through 4, but not for 5 and 6. */ /* -** SETTING: self-register boolean default=off +** SETTING: self-register boolean default=off sensitive ** Allow users to register themselves through the HTTP UI. ** This is useful if you want to see other names than ** "Anonymous" in e.g. ticketing system. On the other hand ** users can not be deleted. */ /* -** SETTING: ssh-command width=40 +** SETTING: ssh-command width=40 sensitive ** The command used to talk to a remote machine with the "ssh://" protocol. */ /* -** SETTING: ssl-ca-location width=40 +** SETTING: ssl-ca-location width=40 sensitive ** The full pathname to a file containing PEM encoded ** CA root certificates, or a directory of certificates ** with filenames formed from the certificate hashes as ** required by OpenSSL. ** @@ -3842,11 +4107,11 @@ ** Checking your platform behaviour is required if the ** exact contents of the CA root is critical for your ** application. */ /* -** SETTING: ssl-identity width=40 +** SETTING: ssl-identity width=40 sensitive ** The full pathname to a file containing a certificate ** and private key in PEM format. Create by concatenating ** the certificate and private key files. ** ** This identity will be presented to SSL servers to @@ -3853,33 +4118,33 @@ ** authenticate this client, in addition to the normal ** password authentication. */ #ifdef FOSSIL_ENABLE_TCL /* -** SETTING: tcl boolean default=off +** SETTING: tcl boolean default=off sensitive ** If enabled Tcl integration commands will be added to the TH1 ** interpreter, allowing arbitrary Tcl expressions and ** scripts to be evaluated from TH1. Additionally, the Tcl ** interpreter will be able to evaluate arbitrary TH1 ** expressions and scripts. */ /* -** SETTING: tcl-setup width=40 block-text +** SETTING: tcl-setup width=40 block-text sensitive ** This is the setup script to be evaluated after creating ** and initializing the Tcl interpreter. By default, this ** is empty and no extra setup is performed. */ #endif /* FOSSIL_ENABLE_TCL */ /* -** SETTING: tclsh width=80 default=tclsh +** SETTING: tclsh width=80 default=tclsh sensitive ** Name of the external TCL interpreter used for such things ** as running the GUI diff viewer launched by the --tk option ** of the various "diff" commands. */ #ifdef FOSSIL_ENABLE_TH1_DOCS /* -** SETTING: th1-docs boolean default=off +** SETTING: th1-docs boolean default=off sensitive ** If enabled, this allows embedded documentation files to contain ** arbitrary TH1 scripts that are evaluated on the server. If native ** Tcl integration is also enabled, this setting has the ** potential to allow anybody with check-in privileges to ** do almost anything that the associated operating system @@ -3932,11 +4197,11 @@ ** of a "fossil clone" or "fossil sync" command. The ** default is false, in which case the -u option is ** needed to clone or sync unversioned files. */ /* -** SETTING: web-browser width=30 +** SETTING: web-browser width=30 sensitive ** A shell command used to launch your preferred ** web browser when given a URL as an argument. ** Defaults to "start" on windows, "open" on Mac, ** and "firefox" on Unix. */ @@ -4058,11 +4323,13 @@ fossil_fatal("cannot set 'manifest' globally"); } if( unsetFlag ){ db_unset(pSetting->name, globalFlag); }else{ + db_protect_only(PROTECT_NONE); db_set(pSetting->name, g.argv[3], globalFlag); + db_protect_pop(); } if( isManifest && g.localOpen ){ manifest_to_disk(db_lget_int("checkout", 0)); } }else{ Index: src/file.c ================================================================== --- src/file.c +++ src/file.c @@ -47,22 +47,21 @@ ** used for files that are under management by a Fossil repository. ExtFILE ** should be used for files that are not under management. SymFILE is for ** a few special cases such as the "fossil test-tarball" command when we never ** want to follow symlinks. ** -** If RepoFILE is used and if the allow-symlinks setting is true and if -** the object is a symbolic link, then the object is treated like an ordinary -** file whose content is name of the object to which the symbolic link -** points. -** -** If ExtFILE is used or allow-symlinks is false, then operations on a -** symbolic link are the same as operations on the object to which the -** symbolic link points. -** -** SymFILE is like RepoFILE except that it always uses the target filename of -** a symbolic link as the content, instead of the content of the object -** that the symlink points to. SymFILE acts as if allow-symlinks is always ON. +** ExtFILE Symbolic links always refer to the object to which the +** link points. Symlinks are never recognized as symlinks but +** instead always appear to the the target object. +** +** SymFILE Symbolic links always appear to be files whose name is +** the target pathname of the symbolic link. +** +** RepoFILE Like symfile is allow-symlinks is true, or like +** ExtFile if allow-symlinks is false. In other words, +** symbolic links are only recognized as something different +** from files or directories if allow-symlinks is true. */ #define ExtFILE 0 /* Always follow symlinks */ #define RepoFILE 1 /* Follow symlinks if and only if allow-symlinks is OFF */ #define SymFILE 2 /* Never follow symlinks */ @@ -134,13 +133,16 @@ int eFType /* Look at symlink itself if RepoFILE and enabled. */ ){ int rc; void *zMbcs = fossil_utf8_to_path(zFilename, 0); #if !defined(_WIN32) - if( eFType>=RepoFILE && (eFType==SymFILE || db_allow_symlinks()) ){ + if( (eFType=RepoFILE && db_allow_symlinks()) + || eFType==SymFILE ){ + /* Symlinks look like files whose content is the name of the target */ rc = lstat(zMbcs, buf); }else{ + /* Symlinks look like the object to which they point */ rc = stat(zMbcs, buf); } #else rc = win32_stat(zMbcs, buf, eFType); #endif @@ -316,17 +318,90 @@ /* ** Return TRUE if the named file is a symlink and symlinks are allowed. ** Return false for all other cases. ** -** This routines RepoFILE - that zFilename is always a file under management. +** This routines assumes RepoFILE - that zFilename is always a file +** under management. ** ** On Windows, always return False. */ int file_islink(const char *zFilename){ return file_perm(zFilename, RepoFILE)==PERM_LNK; } + +/* +** Check every sub-directory of zRoot along the path to zFile. +** If any sub-directory is really an ordinary file or a symbolic link, +** return an integer which is the length of the prefix of zFile which +** is the name of that object. Return 0 if all no non-directory +** objects are found along the path. +** +** Example: Given inputs +** +** zRoot = /home/alice/project1 +** zFile = /home/alice/project1/main/src/js/fileA.js +** +** Look for objects in the following order: +** +** /home/alice/project/main +** /home/alice/project/main/src +** /home/alice/project/main/src/js +** +** If any of those objects exist and are something other than a directory +** then return the length of the name of the first non-directory object +** seen. +*/ +int file_nondir_objects_on_path(const char *zRoot, const char *zFile){ + int i = (int)strlen(zRoot); + char *z = fossil_strdup(zFile); + assert( fossil_strnicmp(zRoot, z, i)==0 ); + if( i && zRoot[i-1]=='/' ) i--; + while( z[i]=='/' ){ + int j, rc; + for(j=i+1; z[j] && z[j]!='/'; j++){} + if( z[j]!='/' ) break; + z[j] = 0; + rc = file_isdir(z, SymFILE); + if( rc!=1 ){ + if( rc==2 ){ + fossil_free(z); + return j; + } + break; + } + z[j] = '/'; + i = j; + } + fossil_free(z); + return 0; +} + +/* +** The file named zFile is suppose to be an in-tree file. Check to +** ensure that it will be safe to write to this file by verifying that +** there are no symlinks or other non-directory objects in between the +** root of the checkout and zFile. +** +** If a problem is found, print a warning message (using fossil_warning()) +** and return non-zero. If everything is ok, return zero. +*/ +int file_unsafe_in_tree_path(const char *zFile){ + int n; + if( !file_is_absolute_path(zFile) ){ + fossil_panic("%s is not an absolute pathname",zFile); + } + if( fossil_strnicmp(g.zLocalRoot, zFile, (int)strlen(g.zLocalRoot)) ){ + fossil_panic("%s is not a prefix of %s", g.zLocalRoot, zFile); + } + n = file_nondir_objects_on_path(g.zLocalRoot, zFile); + if( n ){ + fossil_warning("cannot write to %s because non-directory object %.*s" + " is in the way", zFile, n, zFile); + } + return n; +} /* ** Return 1 if zFilename is a directory. Return 0 if zFilename ** does not exist. Return 2 if zFilename exists but is something ** other than a directory. @@ -570,11 +645,14 @@ */ int file_setexe(const char *zFilename, int onoff){ int rc = 0; #if !defined(_WIN32) struct stat buf; - if( fossil_stat(zFilename, &buf, RepoFILE)!=0 || S_ISLNK(buf.st_mode) ){ + if( fossil_stat(zFilename, &buf, RepoFILE)!=0 + || S_ISLNK(buf.st_mode) + || S_ISDIR(buf.st_mode) + ){ return 0; } if( onoff ){ int targetMode = (buf.st_mode & 0444)>>2; if( (buf.st_mode & 0100)==0 ){ @@ -1236,12 +1314,12 @@ sqlite3_int64 iMtime; struct fossilStat testFileStat; memset(zBuf, 0, sizeof(zBuf)); blob_zero(&x); file_canonical_name(zPath, &x, slash); - fossil_print("[%s] -> [%s]\n", zPath, blob_buffer(&x)); - blob_reset(&x); + char *zFull = blob_str(&x); + fossil_print("[%s] -> [%s]\n", zPath, zFull); memset(&testFileStat, 0, sizeof(struct fossilStat)); rc = fossil_stat(zPath, &testFileStat, 0); fossil_print(" stat_rc = %d\n", rc); sqlite3_snprintf(sizeof(zBuf), zBuf, "%lld", testFileStat.st_size); fossil_print(" stat_size = %s\n", zBuf); @@ -1285,10 +1363,13 @@ fossil_print(" file_isfile_or_link = %d\n", file_isfile_or_link(zPath)); fossil_print(" file_islink = %d\n", file_islink(zPath)); fossil_print(" file_isexe(RepoFILE) = %d\n", file_isexe(zPath,RepoFILE)); fossil_print(" file_isdir(RepoFILE) = %d\n", file_isdir(zPath,RepoFILE)); fossil_print(" file_is_repository = %d\n", file_is_repository(zPath)); + fossil_print(" file_is_reserved_name = %d\n", + file_is_reserved_name(zFull,-1)); + blob_reset(&x); if( reset ) resetStat(); } /* ** COMMAND: test-file-environment @@ -1300,32 +1381,45 @@ ** ** Options: ** ** --allow-symlinks BOOLEAN Temporarily turn allow-symlinks on/off ** --open-config Open the configuration database first. -** --slash Trailing slashes, if any, are retained. ** --reset Reset cached stat() info for each file. +** --root ROOT Use ROOT as the root of the checkout +** --slash Trailing slashes, if any, are retained. */ void cmd_test_file_environment(void){ int i; int slashFlag = find_option("slash",0,0)!=0; int resetFlag = find_option("reset",0,0)!=0; + const char *zRoot = find_option("root",0,1); const char *zAllow = find_option("allow-symlinks",0,1); if( find_option("open-config", 0, 0)!=0 ){ Th_OpenConfig(1); } db_find_and_open_repository(OPEN_ANY_SCHEMA|OPEN_OK_NOT_FOUND, 0); fossil_print("filenames_are_case_sensitive() = %d\n", filenames_are_case_sensitive()); - fossil_print("db_allow_symlinks_by_default() = %d\n", - db_allow_symlinks_by_default()); if( zAllow ){ g.allowSymlinks = !is_false(zAllow); } + if( zRoot==0 ) zRoot = g.zLocalRoot; fossil_print("db_allow_symlinks() = %d\n", db_allow_symlinks()); + fossil_print("local-root = [%s]\n", zRoot); for(i=2; i=12 ){ /* strlen("_FOSSIL_-(shm|wal)") */ + /* Check for (-wal, -shm, -journal) suffixes, with an eye towards + ** runtime speed. */ + if( zEnd[-4]=='-' ){ + if( fossil_strnicmp("wal", &zEnd[-3], 3) + && fossil_strnicmp("shm", &zEnd[-3], 3) ){ + return 0; + } + gotSuffix = 4; + }else if( nFilename>=16 && zEnd[-8]=='-' ){ /*strlen(_FOSSIL_-journal) */ + if( fossil_strnicmp("journal", &zEnd[-7], 7) ) return 0; + gotSuffix = 8; + } + if( gotSuffix ){ + assert( 4==gotSuffix || 8==gotSuffix ); + zEnd -= gotSuffix; + nFilename -= gotSuffix; + gotSuffix = 1; + } + assert( nFilename>=8 && "strlen(_FOSSIL_)" ); + assert( gotSuffix==0 || gotSuffix==1 ); + } + switch( zEnd[-1] ){ + case '_':{ + if( fossil_strnicmp("_FOSSIL_", &zEnd[-8], 8) ) return 0; + if( 8==nFilename ) return 1; + return zEnd[-9]=='/' ? 2 : gotSuffix; + } + case 'T': + case 't':{ + if( nFilename<9 || zEnd[-9]!='.' + || fossil_strnicmp(".fslckout", &zEnd[-9], 9) ){ + return 0; + } + if( 9==nFilename ) return 1; + return zEnd[-10]=='/' ? 2 : gotSuffix; + } + default:{ + return 0; + } + } +} Index: src/forum.c ================================================================== --- src/forum.c +++ src/forum.c @@ -1114,13 +1114,15 @@ moderation_approve('f', fpid); if( g.perm.AdminForum && PB("trust") && (zUserToTrust = P("trustuser"))!=0 ){ + db_unprotect(PROTECT_USER); db_multi_exec("UPDATE user SET cap=cap||'4' " "WHERE login=%Q AND cap NOT GLOB '*4*'", zUserToTrust); + db_protect_pop(); } cgi_redirectf("%R/forumpost/%S",P("fpid")); return; } if( P("reject") ){ Index: src/hook.c ================================================================== --- src/hook.c +++ src/hook.c @@ -123,15 +123,17 @@ ** If N==0, then there is no expectation of new artifacts arriving ** soon and so post-receive hooks can be run without delay. */ void hook_expecting_more_artifacts(int N){ if( N>0 ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name,value,mtime)" "VALUES('hook-embargo',now()+%d,now())", N ); + db_protect_pop(); }else{ db_unset("hook-embargo",0); } } @@ -243,10 +245,11 @@ fossil_fatal("the --command and --type options are required"); } validate_type(zType); nSeq = zSeq ? atoi(zSeq) : 10; db_begin_write(); + db_unprotect(PROTECT_CONFIG); db_multi_exec( "INSERT OR IGNORE INTO config(name,value) VALUES('hooks','[]');\n" "UPDATE config" " SET value=json_insert(" " CASE WHEN json_valid(value) THEN value ELSE '[]' END,'$[#]'," @@ -253,10 +256,11 @@ " json_object('cmd',%Q,'type',%Q,'seq',%d))," " mtime=now()" " WHERE name='hooks';", zCmd, zType, nSeq ); + db_protect_pop(); db_commit_transaction(); }else if( strncmp(zCmd, "edit", nCmd)==0 ){ const char *zCmd = find_option("command",0,1); const char *zType = find_option("type",0,1); @@ -290,20 +294,23 @@ } if( zSeq ){ blob_append_sql(&sql, ",'$[%d].seq',%d", id, nSeq); } blob_append_sql(&sql,") WHERE name='hooks';"); + db_unprotect(PROTECT_CONFIG); db_multi_exec("%s", blob_sql_text(&sql)); + db_protect_pop(); blob_reset(&sql); } db_commit_transaction(); }else if( strncmp(zCmd, "delete", nCmd)==0 ){ int i; verify_all_options(); if( g.argc<4 ) usage("delete ID ..."); db_begin_write(); + db_unprotect(PROTECT_CONFIG); db_multi_exec( "INSERT OR IGNORE INTO config(name,value) VALUES('hooks','[]');\n" ); for(i=3; iInterwiki links are hyperlink targets of the form Index: src/json_config.c ================================================================== --- src/json_config.c +++ src/json_config.c @@ -83,11 +83,10 @@ { "keep-glob", CONFIGSET_PROJ }, { "crlf-glob", CONFIGSET_PROJ }, { "crnl-glob", CONFIGSET_PROJ }, { "encoding-glob", CONFIGSET_PROJ }, { "empty-dirs", CONFIGSET_PROJ }, -{ "allow-symlinks", CONFIGSET_PROJ }, { "dotfiles", CONFIGSET_PROJ }, { "ticket-table", CONFIGSET_TKT }, { "ticket-common", CONFIGSET_TKT }, { "ticket-change", CONFIGSET_TKT }, Index: src/json_user.c ================================================================== --- src/json_user.c +++ src/json_user.c @@ -212,13 +212,15 @@ json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS, "User %s already exists.", zName); goto error; }else{ Stmt ins = empty_Stmt; + db_unprotect(PROTECT_USER); db_prepare(&ins, "INSERT INTO user (login) VALUES(%Q)",zName); db_step( &ins ); db_finalize(&ins); + db_protect_pop(); uid = db_int(0,"SELECT uid FROM user WHERE login=%Q", zName); assert(uid>0); zNameNew = zName; cson_object_set( pUser, "uid", cson_value_new_integer(uid) ); } @@ -345,13 +347,15 @@ #endif #if 0 puts(blob_str(&sql)); cson_output_FILE( cson_object_value(pUser), stdout, NULL ); #endif + db_unprotect(PROTECT_USER); db_prepare(&q, "%s", blob_sql_text(&sql)); db_exec(&q); db_finalize(&q); + db_protect_pop(); #if TRY_LOGIN_GROUP if( zPW || cson_value_get_bool(forceLogout) ){ Blob groupSql = empty_blob; char * zErr = NULL; blob_append_sql(&groupSql, @@ -358,11 +362,13 @@ "INSERT INTO user(login)" " SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);", zName, zName ); blob_append(&groupSql, blob_str(&sql), blob_size(&sql)); + db_unprotect(PROTECT_USER); login_group_sql(blob_str(&groupSql), NULL, NULL, &zErr); + db_protect_pop(); blob_reset(&groupSql); if( zErr ){ json_set_err( FSL_JSON_E_UNKNOWN, "Repo-group update at least partially failed: %s", zErr); Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -293,13 +293,15 @@ if( zHash==0 ) zHash = db_text(0, "SELECT hex(randomblob(25))"); zCookie = login_gen_user_cookie_value(zUsername, zHash); cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), bSessionCookie ? 0 : expires); record_login_attempt(zUsername, zIpAddr, 1); + db_unprotect(PROTECT_USER); db_multi_exec("UPDATE user SET cookie=%Q," " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d", zHash, expires, uid); + db_protect_pop(); fossil_free(zHash); if( zDest ){ *zDest = zCookie; }else{ free(zCookie); @@ -356,14 +358,16 @@ }else{ const char *cookie = login_cookie_name(); /* To logout, change the cookie value to an empty string */ cgi_set_cookie(cookie, "", login_cookie_path(), -86400); + db_unprotect(PROTECT_USER); db_multi_exec("UPDATE user SET cookie=NULL, ipaddr=NULL, " " cexpire=0 WHERE uid=%d" " AND login NOT IN ('anonymous','nobody'," " 'developer','reader')", g.userUid); + db_protect_pop(); cgi_replace_parameter(cookie, NULL); cgi_replace_parameter("anon", NULL); } } @@ -580,22 +584,27 @@ ; }else{ char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0); char *zChngPw; char *zErr; + int rc; + + db_unprotect(PROTECT_USER); db_multi_exec( "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid ); - fossil_free(zNewPw); zChngPw = mprintf( "UPDATE user" " SET pw=shared_secret(%Q,%Q," " (SELECT value FROM config WHERE name='project-code'))" " WHERE login=%Q", zNew1, g.zLogin, g.zLogin ); - if( login_group_sql(zChngPw, "

", "

\n", &zErr) ){ + fossil_free(zNewPw); + rc = login_group_sql(zChngPw, "

", "

\n", &zErr); + db_protect_pop(); + if( rc ){ zErrMsg = mprintf("%s", zErr); fossil_free(zErr); }else{ redirect_to_g(); return; @@ -835,16 +844,18 @@ zLogin, zHash ); pStmt = 0; rc = sqlite3_prepare_v2(pOther, zSQL, -1, &pStmt, 0); if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ + db_unprotect(PROTECT_USER); db_multi_exec( "UPDATE user SET cookie=%Q, cexpire=%.17g" " WHERE login=%Q", zHash, sqlite3_column_double(pStmt, 0), zLogin ); + db_protect_pop(); nXfer++; } sqlite3_finalize(pStmt); } sqlite3_close(pOther); @@ -1619,11 +1630,13 @@ "INSERT INTO user(login,pw,cap,info,mtime)\n" "VALUES(%Q,%Q,%Q," "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())", zUserID, zPass, zStartPerms, zDName, zEAddr, g.zIpAddr); fossil_free(zPass); + db_unprotect(PROTECT_USER); db_multi_exec("%s", blob_sql_text(&sql)); + db_protect_pop(); uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID); login_set_user_cookie(zUserID, uid, NULL, 0); if( doAlerts ){ /* Also make the new user a subscriber. */ Blob hdr, body; @@ -1832,14 +1845,16 @@ while( db_step(&q)==SQLITE_ROW ){ const char *zRepoName = db_column_text(&q, 1); if( file_size(zRepoName, ExtFILE)<0 ){ /* Silently remove non-existent repositories from the login group. */ const char *zLabel = db_column_text(&q, 0); + db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM config WHERE name GLOB 'peer-*-%q'", &zLabel[10] ); + db_protect_pop(); continue; } rc = sqlite3_open_v2( zRepoName, &pPeer, SQLITE_OPEN_READWRITE, @@ -2004,11 +2019,13 @@ "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());" "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());" "COMMIT;", zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo ); + db_unprotect(PROTECT_CONFIG); login_group_sql(zSql, "
  • ", "
  • ", pzErrMsg); + db_protect_pop(); fossil_free(zSql); } /* ** Leave the login group that we are currently part of. @@ -2025,17 +2042,19 @@ " WHERE name='login-group-name'" " AND (SELECT count(*) FROM config WHERE name GLOB 'peer-*')==0;", zProjCode ); fossil_free(zProjCode); + db_unprotect(PROTECT_CONFIG); login_group_sql(zSql, "
  • ", "
  • ", pzErrMsg); fossil_free(zSql); db_multi_exec( "DELETE FROM config " " WHERE name GLOB 'peer-*'" " OR name GLOB 'login-group-*';" ); + db_protect_pop(); } /* ** COMMAND: login-group* ** Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -1372,19 +1372,21 @@ g.zTop = &g.zBaseURL[7+strlen(zHost)]; g.zHttpsURL = mprintf("https://%s%.*s", zHost, i, zCur); } } if( db_is_writeable("repository") ){ + db_unprotect(PROTECT_CONFIG); if( !db_exists("SELECT 1 FROM config WHERE name='baseurl:%q'", g.zBaseURL)){ db_multi_exec("INSERT INTO config(name,value,mtime)" "VALUES('baseurl:%q',1,now())", g.zBaseURL); }else{ db_optional_sql("repository", "REPLACE INTO config(name,value,mtime)" "VALUES('baseurl:%q',1,now())", g.zBaseURL ); } + db_protect_pop(); } } /* ** Send an HTTP redirect back to the designated Index Page. Index: src/manifest.c ================================================================== --- src/manifest.c +++ src/manifest.c @@ -481,14 +481,23 @@ blob_appendf(pErr, "line 1 not recognized"); return 0; } /* Then verify the Z-card. */ +#if 1 + /* Disable this ***ONLY*** (ONLY!) when testing hand-written inputs + for card-related syntax errors. */ if( verify_z_card(z, n, pErr)==2 ){ blob_reset(pContent); return 0; } +#else +#warning ACHTUNG - z-card check is disabled for testing purposes. + if(0 && verify_z_card(NULL, 0, NULL)){ + /*avoid unused static func error*/ + } +#endif /* Allocate a Manifest object to hold the parsed control artifact. */ p = fossil_malloc( sizeof(*p) ); memset(p, 0, sizeof(*p)); @@ -601,10 +610,11 @@ case 'E': { if( p->rEventDate>0.0 ) SYNTAX("more than one E-card"); p->rEventDate = db_double(0.0,"SELECT julianday(%Q)", next_token(&x,0)); if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card"); p->zEventId = next_token(&x, &sz); + if( p->zEventId==0 ) SYNTAX("missing hash on E-card"); if( !hname_validate(p->zEventId, sz) ){ SYNTAX("malformed hash on E-card"); } p->type = CFTYPE_EVENT; break; @@ -625,10 +635,11 @@ if( !file_is_simple_pathname_nonstrict(zName) ){ SYNTAX("F-card filename is not a simple path"); } zUuid = next_token(&x, &sz); if( p->zBaseline==0 || zUuid!=0 ){ + if( zUuid==0 ) SYNTAX("missing hash on F-card"); if( !hname_validate(zUuid,sz) ){ SYNTAX("F-card hash invalid"); } } zPerm = next_token(&x,0); @@ -643,17 +654,24 @@ p->nFileAlloc = p->nFileAlloc*2 + 10; p->aFile = fossil_realloc(p->aFile, p->nFileAlloc*sizeof(p->aFile[0]) ); } i = p->nFile++; + if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){ + SYNTAX("incorrect F-card sort order"); + } + if( file_is_reserved_name(zName,-1) ){ + /* If reserved names leaked into historical manifests due to + ** slack oversight by older versions of Fossil, simply ignore + ** those files */ + p->nFile--; + break; + } p->aFile[i].zName = zName; p->aFile[i].zUuid = zUuid; p->aFile[i].zPerm = zPerm; p->aFile[i].zPrior = zPriorName; - if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){ - SYNTAX("incorrect F-card sort order"); - } p->type = CFTYPE_MANIFEST; break; } /* Index: src/mkindex.c ================================================================== --- src/mkindex.c +++ src/mkindex.c @@ -90,10 +90,11 @@ #define CMDFLAG_SETTING 0x0020 /* A setting */ #define CMDFLAG_VERSIONABLE 0x0040 /* A versionable setting */ #define CMDFLAG_BLOCKTEXT 0x0080 /* Multi-line text setting */ #define CMDFLAG_BOOLEAN 0x0100 /* A boolean setting */ #define CMDFLAG_RAWCONTENT 0x0200 /* Do not interpret webpage content */ +#define CMDFLAG_SENSITIVE 0x0400 /* Security-sensitive setting */ /**************************************************************************/ /* ** Each entry looks like this: */ @@ -248,10 +249,12 @@ }else if( j==10 && strncmp(&zLine[i], "block-text", j)==0 ){ aEntry[nUsed].eType &= ~(CMDFLAG_BOOLEAN); aEntry[nUsed].eType |= CMDFLAG_BLOCKTEXT; }else if( j==11 && strncmp(&zLine[i], "versionable", j)==0 ){ aEntry[nUsed].eType |= CMDFLAG_VERSIONABLE; + }else if( j==9 && strncmp(&zLine[i], "sensitive", j)==0 ){ + aEntry[nUsed].eType |= CMDFLAG_SENSITIVE; }else if( j>6 && strncmp(&zLine[i], "width=", 6)==0 ){ aEntry[nUsed].iWidth = atoi(&zLine[i+6]); }else if( j>8 && strncmp(&zLine[i], "default=", 8)==0 ){ aEntry[nUsed].zDflt = string_dup(&zLine[i+8], j-8); }else if( j>9 && strncmp(&zLine[i], "variable=", 9)==0 ){ @@ -479,14 +482,15 @@ if( zVar ){ printf(" \"%s\",%*s", zVar, (int)(15-strlen(zVar)), ""); }else{ printf(" 0,%*s", 16, ""); } - printf(" %3d, %d, %d, \"%s\"%*s },\n", + printf(" %3d, %d, %d, %d, \"%s\"%*s },\n", aEntry[i].iWidth, (aEntry[i].eType & CMDFLAG_VERSIONABLE)!=0, (aEntry[i].eType & CMDFLAG_BLOCKTEXT)!=0, + (aEntry[i].eType & CMDFLAG_SENSITIVE)!=0, zDef, (int)(10-strlen(zDef)), "" ); if( aEntry[i].zIf ){ printf("#endif\n"); } Index: src/printf.c ================================================================== --- src/printf.c +++ src/printf.c @@ -1148,12 +1148,15 @@ rc = fossil_print_error(rc, z); abort(); exit(rc); } NORETURN void fossil_fatal(const char *zFormat, ...){ + static int once = 0; char *z; int rc = 1; + if( once ) exit(1); + once = 1; va_list ap; mainInFatalError = 1; va_start(ap, zFormat); z = vmprintf(zFormat, ap); va_end(ap); Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -52,10 +52,11 @@ } /* Add the user.mtime column if it is missing. (2011-04-27) */ if( !db_table_has_column("repository", "user", "mtime") ){ + db_unprotect(PROTECT_ALL); db_multi_exec( "CREATE TEMP TABLE temp_user AS SELECT * FROM user;" "DROP TABLE user;" "CREATE TABLE user(\n" " uid INTEGER PRIMARY KEY,\n" @@ -72,19 +73,22 @@ "INSERT OR IGNORE INTO user" " SELECT uid, login, pw, cap, cookie," " ipaddr, cexpire, info, now(), photo FROM temp_user;" "DROP TABLE temp_user;" ); + db_protect_pop(); } /* Add the config.mtime column if it is missing. (2011-04-27) */ if( !db_table_has_column("repository", "config", "mtime") ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "ALTER TABLE config ADD COLUMN mtime INTEGER;" "UPDATE config SET mtime=now();" ); + db_protect_pop(); } /* Add the shun.mtime and shun.scom columns if they are missing. ** (2011-04-27) */ @@ -382,10 +386,11 @@ percent_complete(0); } alert_triggers_disable(); rebuild_update_schema(); blob_init(&sql, 0, 0); + db_unprotect(PROTECT_ALL); db_prepare(&q, "SELECT name FROM sqlite_schema /*scan*/" " WHERE type='table'" " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias'," "'config','shun','private','reportfmt'," @@ -475,10 +480,11 @@ alert_triggers_enable(); if(!g.fQuiet && ttyOutput ){ percent_complete(1000); fossil_print("\n"); } + db_protect_pop(); return errCnt; } /* ** Number of neighbors to search @@ -667,10 +673,11 @@ /* We should be done with options.. */ verify_all_options(); db_begin_transaction(); + db_unprotect(PROTECT_ALL); if( !compressOnlyFlag ){ search_drop_index(); ttyOutput = 1; errCnt = rebuild_db(randomizeFlag, 1, doClustering); reconstruct_private_table(); @@ -720,10 +727,11 @@ if( activateWal ){ db_multi_exec("PRAGMA journal_mode=WAL;"); } } if( runReindex ) search_rebuild_index(); + db_protect_pop(); if( showStats ){ static const struct { int idx; const char *zLabel; } aStat[] = { { CFTYPE_ANY, "Artifacts:" }, { CFTYPE_MANIFEST, "Manifests:" }, { CFTYPE_CLUSTER, "Clusters:" }, @@ -755,18 +763,20 @@ ** testing by cloning a working project repository. */ void test_detach_cmd(void){ db_find_and_open_repository(0, 2); db_begin_transaction(); + db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM config WHERE name GLOB 'last-sync-*';" "DELETE FROM config WHERE name GLOB 'sync-*:*';" "UPDATE config SET value=lower(hex(randomblob(20)))" " WHERE name='project-code';" "UPDATE config SET value='detached-' || value" " WHERE name='project-name' AND value NOT GLOB 'detached-*';" ); + db_protect_pop(); db_end_transaction(0); } /* ** COMMAND: test-create-clusters @@ -910,10 +920,11 @@ if( privateOnly || bVerily ){ bNeedRebuild = db_exists("SELECT 1 FROM private"); delete_private_content(); } if( !privateOnly ){ + db_unprotect(PROTECT_ALL); db_multi_exec( "UPDATE user SET pw='';" "DELETE FROM config WHERE name GLOB 'last-sync-*';" "DELETE FROM config WHERE name GLOB 'sync-*:*';" "DELETE FROM config WHERE name GLOB 'peer-*';" @@ -933,14 +944,17 @@ "DROP TABLE IF EXISTS purgeitem;\n" "DROP TABLE IF EXISTS admin_log;\n" "DROP TABLE IF EXISTS vcache;\n" ); } + db_protect_pop(); } if( !bNeedRebuild ){ db_end_transaction(0); + db_unprotect(PROTECT_ALL); db_multi_exec("VACUUM;"); + db_protect_pop(); }else{ rebuild_db(0, 1, 0); db_end_transaction(0); } } Index: src/report.c ================================================================== --- src/report.c +++ src/report.c @@ -230,15 +230,15 @@ /* ** Activate the query authorizer */ void report_restrict_sql(char **pzErr){ - sqlite3_set_authorizer(g.db, report_query_authorizer, (void*)pzErr); + db_set_authorizer(report_query_authorizer,(void*)pzErr,"Ticket-Report"); sqlite3_limit(g.db, SQLITE_LIMIT_VDBE_OP, 10000); } void report_unrestrict_sql(void){ - sqlite3_set_authorizer(g.db, 0, 0); + db_clear_authorizer(); } /* ** Check the given SQL to see if is a valid query that does not @@ -680,11 +680,11 @@ */ if( pState->nCount==0 ){ /* Turn off the authorizer. It is no longer doing anything since the ** query has already been prepared. */ - sqlite3_set_authorizer(g.db, 0, 0); + db_clear_authorizer(); /* Figure out the number of columns, the column that determines background ** color, and whether or not this row of data is represented by multiple ** rows in the table. */ Index: src/security_audit.c ================================================================== --- src/security_audit.c +++ src/security_audit.c @@ -281,10 +281,18 @@ @

    Fix this by removing the "Mod-Wiki", "Mod-Tkt", and "Mod-Forum" @ privileges (capabilities "fq5") @ from users "anonymous" and "nobody" @ on the User Configuration page. } + + /* The strict-manifest-syntax setting should be on. */ + if( db_get_boolean("strict-manifest-syntax",1)==0 ){ + @

  • WARNING: + @ The "strict-manifest-syntax" flag is off. This is a security + @ risk. Turn this setting on (its default) to protect the users + @ of this repository. + } /* Obsolete: */ if( hasAnyCap(zAnonCap, "d") || hasAnyCap(zDevCap, "d") || hasAnyCap(zReadCap, "d") ){ @@ -596,15 +604,17 @@ if( P("cancel") ){ /* User pressed the cancel button. Go back */ cgi_redirect("secaudit0"); } if( P("apply") ){ + db_unprotect(PROTECT_USER); db_multi_exec( "UPDATE user SET cap=''" " WHERE login IN ('nobody','anonymous');" "DELETE FROM config WHERE name='public-pages';" ); + db_protect_pop(); db_set("self-register","0",0); cgi_redirect("secaudit0"); } style_header("Make This Website Private"); @

    Click the "Make It Private" button below to disable all Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -27,14 +27,16 @@ */ void setup_incr_cfgcnt(void){ static int once = 1; if( once ){ once = 0; + db_unprotect(PROTECT_CONFIG); db_multi_exec("UPDATE config SET value=value+1 WHERE name='cfgcnt'"); if( db_changes()==0 ){ db_multi_exec("INSERT INTO config(name,value) VALUES('cfgcnt',1)"); } + db_protect_pop(); } } /* ** Output a single entry for a menu generated using an HTML table. @@ -195,11 +197,13 @@ } if( zQ ){ int iQ = fossil_strcmp(zQ,"on")==0 || atoi(zQ); if( iQ!=iVal ){ login_verify_csrf_secret(); + db_protect_only(PROTECT_NONE); db_set(zVar, iQ ? "1" : "0", 0); + db_protect_pop(); setup_incr_cfgcnt(); admin_log("Set option [%q] to [%q].", zVar, iQ ? "on" : "off"); iVal = iQ; } @@ -230,11 +234,13 @@ const char *zQ = P(zQParm); if( zQ && fossil_strcmp(zQ,zVal)!=0 ){ const int nZQ = (int)strlen(zQ); login_verify_csrf_secret(); setup_incr_cfgcnt(); + db_protect_only(PROTECT_NONE); db_set(zVar, zQ, 0); + db_protect_pop(); admin_log("Set entry_attribute %Q to: %.*s%s", zVar, 20, zQ, (nZQ>20 ? "..." : "")); zVal = zQ; } @ 20 ? "..." : "")); z = zQ; } @@ -1162,11 +1170,13 @@ login_needed(0); return; } db_begin_transaction(); if( P("clear")!=0 && cgi_csrf_safe(1) ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name GLOB 'adunit*'"); + db_protect_pop(); cgi_replace_parameter("adunit",""); cgi_replace_parameter("adright",""); setup_incr_cfgcnt(); } @@ -1260,10 +1270,11 @@ if( !g.perm.Admin ){ login_needed(0); return; } db_begin_transaction(); + db_unprotect(PROTECT_CONFIG); if( !cgi_csrf_safe(1) ){ /* Allow no state changes if not safe from CSRF */ }else if( P("setlogo")!=0 && zLogoMime && zLogoMime[0] && szLogoImg>0 ){ Blob img; Stmt ins; @@ -1290,10 +1301,11 @@ cgi_redirect("setup_logo"); }else if( P("setbg")!=0 && zBgMime && zBgMime[0] && szBgImg>0 ){ Blob img; Stmt ins; blob_init(&img, aBgImg, szBgImg); + db_unprotect(PROTECT_CONFIG); db_prepare(&ins, "REPLACE INTO config(name,value,mtime)" " VALUES('background-image',:bytes,now())" ); db_bind_blob(&ins, ":bytes", &img); @@ -1302,13 +1314,15 @@ db_multi_exec( "REPLACE INTO config(name,value,mtime)" " VALUES('background-mimetype',%Q,now())", zBgMime ); + db_protect_pop(); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("clrbg")!=0 ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM config WHERE name IN " "('background-image','background-mimetype')" ); db_end_transaction(0); @@ -1315,10 +1329,11 @@ cgi_redirect("setup_logo"); }else if( P("seticon")!=0 && zIconMime && zIconMime[0] && szIconImg>0 ){ Blob img; Stmt ins; blob_init(&img, aIconImg, szIconImg); + db_unprotect(PROTECT_CONFIG); db_prepare(&ins, "REPLACE INTO config(name,value,mtime)" " VALUES('icon-image',:bytes,now())" ); db_bind_blob(&ins, ":bytes", &img); @@ -1327,10 +1342,11 @@ db_multi_exec( "REPLACE INTO config(name,value,mtime)" " VALUES('icon-mimetype',%Q,now())", zIconMime ); + db_protect_pop(); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("clricon")!=0 ){ db_multi_exec( "DELETE FROM config WHERE name IN " @@ -1786,22 +1802,27 @@ const char *zValue ){ if( !cgi_csrf_safe(1) ) return; if( zNewName[0]==0 || zValue[0]==0 ){ if( zOldName[0] ){ + db_unprotect(PROTECT_CONFIG); blob_append_sql(pSql, "DELETE FROM config WHERE name='walias:%q';\n", zOldName); + db_protect_pop(); } return; } if( zOldName[0]==0 ){ + db_unprotect(PROTECT_CONFIG); blob_append_sql(pSql, "INSERT INTO config(name,value,mtime) VALUES('walias:%q',%Q,now());\n", zNewName, zValue); + db_protect_pop(); return; } + db_unprotect(PROTECT_CONFIG); if( strcmp(zOldName, zNewName)!=0 ){ blob_append_sql(pSql, "UPDATE config SET name='walias:%q', value=%Q, mtime=now()" " WHERE name='walias:%q';\n", zNewName, zValue, zOldName); @@ -1809,10 +1830,11 @@ blob_append_sql(pSql, "UPDATE config SET value=%Q, mtime=now()" " WHERE name='walias:%q' AND value<>%Q;\n", zValue, zOldName, zValue); } + db_protect_pop(); } /* ** WEBPAGE: waliassetup ** Index: src/setupuser.c ================================================================== --- src/setupuser.c +++ src/setupuser.c @@ -315,11 +315,13 @@ /* Check for requests to delete the user */ if( P("delete") && cgi_csrf_safe(1) ){ int n; if( P("verifydelete") ){ /* Verified delete user request */ + db_unprotect(PROTECT_USER); db_multi_exec("DELETE FROM user WHERE uid=%d", uid); + db_protect_pop(); moderation_disapprove_for_missing_users(); admin_log("Deleted user [%s] (uid %d).", PD("login","???")/*safe-for-%s*/, uid); cgi_redirect(cgi_referer("setup_ulist")); return; @@ -401,15 +403,17 @@ @ [Bummer]

    style_footer(); return; } login_verify_csrf_secret(); + db_unprotect(PROTECT_USER); db_multi_exec( "REPLACE INTO user(uid,login,info,pw,cap,mtime) " "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now())", uid, zLogin, P("info"), zPw, zCap ); + db_protect_pop(); setup_incr_cfgcnt(); admin_log( "Updated user [%q] with capabilities [%q].", zLogin, zCap ); if( atoi(PD("all","0"))>0 ){ Blob sql; @@ -432,11 +436,13 @@ " mtime=now()" " WHERE login=%Q;", zLogin, P("pw"), zLogin, P("info"), zCap, zOldLogin ); + db_unprotect(PROTECT_USER); login_group_sql(blob_str(&sql), "
  • ", "
  • \n", &zErr); + db_protect_pop(); blob_reset(&sql); admin_log( "Updated user [%q] in all login groups " "with capabilities [%q].", zLogin, zCap ); if( zErr ){ Index: src/skins.c ================================================================== --- src/skins.c +++ src/skins.c @@ -360,14 +360,16 @@ zLabel = mprintf("skins/default/%s.txt", azSkinFile[i]); z = builtin_text(zLabel); fossil_free(zLabel); } } + db_unprotect(PROTECT_CONFIG); blob_appendf(&val, "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n", azSkinFile[i], z ); + db_protect_pop(); } return blob_str(&val); } /* @@ -402,14 +404,16 @@ login_insert_csrf_secret(); @ style_footer(); return 1; } + db_unprotect(PROTECT_CONFIG); db_multi_exec( "UPDATE config SET name='skin:%q' WHERE name='skin:%q';", zNewName, zOldName ); + db_protect_pop(); return 0; } /* ** Respond to a Save button press. Return TRUE if a dialog was painted. @@ -440,15 +444,17 @@ login_insert_csrf_secret(); @ style_footer(); return 1; } + db_unprotect(PROTECT_CONFIG); db_multi_exec( "INSERT OR IGNORE INTO config(name, value, mtime)" "VALUES('skin:%q',%Q,now())", zNewName, zCurrent ); + db_protect_pop(); return 0; } /* ** WEBPAGE: setup_skin_admin @@ -491,16 +497,20 @@ style_footer(); db_end_transaction(1); return; } if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + db_protect_pop(); } if( P("draftdel")!=0 ){ const char *zDraft = P("name"); if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft); + db_protect_pop(); } } if( skinRename() || skinSave(zCurrent) ){ db_end_transaction(0); return; @@ -521,15 +531,17 @@ } if( !seen ){ seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'" " AND value=%Q", zCurrent); if( !seen ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "INSERT INTO config(name,value,mtime) VALUES(" " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S')," " %Q,now())", zCurrent ); + db_protect_pop(); } } seen = 0; for(i=0; i=0; new_link = file_islink(0); if( new_exists ){ blob_read_from_file(¤t, zFullname, RepoFILE); @@ -69,11 +69,13 @@ old_exists = db_column_int(&q, 1); old_exe = db_column_int(&q, 2); if( old_exists ){ db_ephemeral_blob(&q, 0, &new); } - if( old_exists ){ + if( file_unsafe_in_tree_path(zFullname) ){ + /* do nothign with this unsafe file */ + }else if( old_exists ){ if( new_exists ){ fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname); }else{ fossil_print("NEW %s\n", zPathname); } Index: src/update.c ================================================================== --- src/update.c +++ src/update.c @@ -927,10 +927,12 @@ " SET pathname=origname, origname=NULL" " WHERE pathname=%Q AND origname!=pathname;" "DELETE FROM vfile WHERE pathname=%Q", zFile, zFile ); + }else if( file_unsafe_in_tree_path(zFull) ){ + /* Ignore this file */ }else{ sqlite3_int64 mtime; int rvChnged = 0; int rvPerm = manifest_file_mperm(pRvFile); Index: src/url.c ================================================================== --- src/url.c +++ src/url.c @@ -50,11 +50,11 @@ int isHttps; /* True if a "https:" url */ int isSsh; /* True if an "ssh:" url */ int isAlias; /* Input URL was an alias */ char *name; /* Hostname for http: or filename for file: */ char *hostname; /* The HOST: parameter on http headers */ - const char *protocol; /* "http" or "https" or "ssh" */ + const char *protocol; /* "http" or "https" or "ssh" or "file" */ int port; /* TCP port number for http: or https: */ int dfltPort; /* The default port for the given protocol */ char *path; /* Pathname for http: */ char *user; /* User id for http: */ char *passwd; /* Password for http: */ @@ -76,11 +76,11 @@ ** as follows: ** ** isFile True if FILE: ** isHttps True if HTTPS: ** isSsh True if SSH: -** protocol "http" or "https" or "file" +** protocol "http" or "https" or "file" or "ssh" ** name Hostname for HTTP:, HTTPS:, SSH:. Filename for FILE: ** port TCP port number for HTTP or HTTPS. ** dfltPort Default TCP port number (80 or 443). ** path Path name for HTTP or HTTPS. ** user Userid. @@ -305,11 +305,11 @@ ** form last-sync-pw. ** ** g.url.isFile True if FILE: ** g.url.isHttps True if HTTPS: ** g.url.isSsh True if SSH: -** g.url.protocol "http" or "https" or "file" +** g.url.protocol "http" or "https" or "file" or "ssh" ** g.url.name Hostname for HTTP:, HTTPS:, SSH:. Filename for FILE: ** g.url.port TCP port number for HTTP or HTTPS. ** g.url.dfltPort Default TCP port number (80 or 443). ** g.url.path Path name for HTTP or HTTPS. ** g.url.user Userid. Index: src/user.c ================================================================== --- src/user.c +++ src/user.c @@ -432,12 +432,14 @@ } if( blob_size(&pw)==0 ){ fossil_print("password unchanged\n"); }else{ char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); + db_unprotect(PROTECT_USER); db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", zSecret, uid); + db_protect_pop(); free(zSecret); } }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ int uid; if( g.argc!=4 && g.argc!=5 ){ @@ -446,14 +448,16 @@ uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]); if( uid==0 ){ fossil_fatal("no such user: %s", g.argv[3]); } if( g.argc==5 ){ + db_unprotect(PROTECT_USER); db_multi_exec( "UPDATE user SET cap=%Q, mtime=now() WHERE uid=%d", g.argv[4], uid ); + db_protect_pop(); } fossil_print("%s\n", db_text(0, "SELECT cap FROM user WHERE uid=%d", uid)); }else{ fossil_fatal("user subcommand should be one of: " "capabilities default list new password"); @@ -573,10 +577,11 @@ void user_hash_passwords_cmd(void){ if( g.argc!=3 ) usage("REPOSITORY"); db_open_repository(g.argv[2]); sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0, sha1_shared_secret_sql_function, 0, 0); + db_unprotect(PROTECT_ALL); db_multi_exec( "UPDATE user SET pw=shared_secret(pw,login), mtime=now()" " WHERE length(pw)>0 AND length(pw)!=40" ); } Index: src/vfile.c ================================================================== --- src/vfile.c +++ src/vfile.c @@ -313,10 +313,13 @@ id = db_column_int(&q, 0); zName = db_column_text(&q, 1); rid = db_column_int(&q, 2); isExe = db_column_int(&q, 3); isLink = db_column_int(&q, 4); + if( file_unsafe_in_tree_path(zName) ){ + continue; + } content_get(rid, &content); if( file_is_the_same(&content, zName) ){ blob_reset(&content); if( file_setexe(zName, isExe) ){ db_multi_exec("UPDATE vfile SET mtime=%lld WHERE id=%d", Index: src/xfer.c ================================================================== --- src/xfer.c +++ src/xfer.c @@ -1657,11 +1657,13 @@ int x = db_column_int(&q,3); const char *zName = db_column_text(&q,4); if( db_column_int64(&q,1)<=iNow-maxAge || !is_a_leaf(x) ){ /* check-in locks expire after maxAge seconds, or when the ** check-in is no longer a leaf */ + db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + db_protect_pop(); continue; } if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){ const char *zClientId = db_column_text(&q, 2); const char *zLogin = db_column_text(&q,0); @@ -1672,16 +1674,18 @@ seenFault = 1; } } db_finalize(&q); if( !seenFault ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name,value,mtime)" "VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())", blob_str(&xfer.aToken[2]), g.zLogin, blob_str(&xfer.aToken[3]) ); + db_protect_pop(); } if( db_get_boolean("forbid-delta-manifests",0) ){ @ pragma avoid-delta-manifests } } @@ -1694,16 +1698,18 @@ */ if( blob_eq(&xfer.aToken[1], "ci-unlock") && xfer.nToken==3 && blob_is_hname(&xfer.aToken[2]) ){ + db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM config" " WHERE name GLOB 'ci-lock-*'" " AND json_extract(value,'$.clientid')=%Q", blob_str(&xfer.aToken[2]) ); + db_protect_pop(); } }else /* Unknown message ADDED test/reserved-names.test Index: test/reserved-names.test ================================================================== --- /dev/null +++ test/reserved-names.test @@ -0,0 +1,121 @@ +# +# Copyright (c) 2020 D. Richard Hipp +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Simplified BSD License (also +# known as the "2-Clause License" or "FreeBSD License".) +# +# 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. +# +# Author contact information: +# drh@hwaci.com +# http://www.hwaci.com/drh/ +# +############################################################################ +# +# Tests for reserved names. +# + +test_setup + +############################################################################### + +set reserved_names_tests [list \ + {0 {}} \ + {0 a.fslckout} \ + {1 .fslckout} \ + {1 .FSlckOUT} \ + {2 a/.fslckout} \ + {0 .fslckout/b} \ + {0 fslckout} \ + {0 .fslckoutx} \ + {1 _FOSSIL_} \ + {0 _FOSSIL} \ + {0 FOSSIL_} \ + {0 FOSSIL_} \ + {0 a_FOSSIL_} \ + {0 _FOSSIL__} \ + {0 __FOSSIL__} \ + {0 __FOssIL__} \ + {0 _FOSSIL_/a} \ + {2 a/_FOSSIL_} \ + {2 _FOSSIL_/c/.fslckout} \ + {2 _FOSSIL_/c/.fslckout/_FOSSIL_} \ + {0 _FOSSIL_/c/.fslckout/._FOSSIL_t} \ + {0 _FOSSIL_/c/.fslckout/t._FOSSIL_} \ + {0 a} \ + {0 a/b} \ + {0 a/b/c} \ + {0 a/b/c/} \ + {0 a/_FOSSIL/} \ + {0 a/fslckout/} \ + {0 a/_fslckout/} \ + {0 _FOSSIL-wal} \ + {0 _FOSSIL-shm} \ + {0 _FOSSIL-journal} \ + {0 _FOSSIL_-wal/a} \ + {0 _FOSSIL_-shm/a} \ + {0 _FOSSIL_-journal/a} \ + {1 _FOSSIL_-wal} \ + {1 _FOSSIL_-shm} \ + {1 _FOSSIL_-journal} \ + {2 a/_FOSSIL_-wal} \ + {2 a/_FOSSIL_-shm} \ + {2 a/_FOSSIL_-journal} \ + {0 .fslckout-wal/a} \ + {0 .fslckout-shm/a} \ + {0 .fslckout-journal/a} \ + {1 .fslckout-wal} \ + {1 .fslckout-shm} \ + {1 .fslckout-journal} \ + {2 a/.fslckout-wal} \ + {2 a/.fslckout-shm} \ + {2 a/.fslckout-journal} \ +] + +############################################################################### + +set testNo 0 + +foreach reserved_names_test $reserved_names_tests { + incr testNo + + set reserved_result [lindex $reserved_names_test 0] + set reserved_name [lindex $reserved_names_test 1] + + fossil test-is-reserved-name $reserved_name + + test reserved-result-$testNo { + [lindex [normalize_result] 0] eq $reserved_result + } + + test reserved-name-$testNo { + [lindex [normalize_result] 1] eq $reserved_name + } + + fossil test-is-reserved-name [string toupper $reserved_name] + + test reserved-result-upper-$testNo { + [lindex [normalize_result] 0] eq $reserved_result + } + + test reserved-name-upper-$testNo { + [lindex [normalize_result] 1] eq [string toupper $reserved_name] + } + + fossil test-is-reserved-name [string tolower $reserved_name] + + test reserved-result-lower-$testNo { + [lindex [normalize_result] 0] eq $reserved_result + } + + test reserved-name-lower-$testNo { + [lindex [normalize_result] 1] eq [string tolower $reserved_name] + } +} + +############################################################################### + +test_cleanup