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/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -856,12 +856,14 @@ if( zIgnoreFlag==0 ){ zIgnoreFlag = db_get("ignore-glob", 0); } pIgnore = glob_create(zIgnoreFlag); +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS /* Always consider symlinks. */ g.allowSymlinks = db_allow_symlinks_by_default(); +#endif locate_unmanaged_files(g.argc-2, g.argv+2, scanFlags, pIgnore); glob_free(pIgnore); blob_zero(&report); status_report(&report, flags); @@ -1015,12 +1017,14 @@ verify_all_options(); pIgnore = glob_create(zIgnoreFlag); pKeep = glob_create(zKeepFlag); pClean = glob_create(zCleanFlag); nRoot = (int)strlen(g.zLocalRoot); +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS /* Always consider symlinks. */ g.allowSymlinks = db_allow_symlinks_by_default(); +#endif if( !dirsOnlyFlag ){ Stmt q; Blob repo; if( !dryRunFlag && !disableUndo ) undo_begin(); locate_unmanaged_files(g.argc-2, g.argv+2, scanFlags, pIgnore); Index: src/configure.c ================================================================== --- src/configure.c +++ src/configure.c @@ -143,11 +143,13 @@ { "keep-glob", CONFIGSET_PROJ }, { "crlf-glob", CONFIGSET_PROJ }, { "crnl-glob", CONFIGSET_PROJ }, { "encoding-glob", CONFIGSET_PROJ }, { "empty-dirs", CONFIGSET_PROJ }, +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS { "allow-symlinks", CONFIGSET_PROJ }, +#endif { "dotfiles", CONFIGSET_PROJ }, { "parent-project-code", CONFIGSET_PROJ }, { "parent-project-name", CONFIGSET_PROJ }, { "hash-policy", CONFIGSET_PROJ }, { "comment-format", CONFIGSET_PROJ }, Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -130,10 +130,13 @@ char *azBeforeCommit[5]; /* Commands to run prior to COMMIT */ int nBeforeCommit; /* Number of entries in azBeforeCommit */ int nPriorChanges; /* sqlite3_total_changes() at transaction start */ const char *zStartFile; /* File in which transaction was started */ int iStartLine; /* Line of zStartFile where transaction started */ + int (*xAuth)(void*,int,const char*,const char*,const char*,const char*); + void *pAuthArg; /* Argument to the authorizer */ + const char *zAuthName; /* Name of the authorizer */ } db = {0, 0, 0, 0, 0, 0, }; /* ** Arrange for the given file to be deleted on a failure. */ @@ -316,10 +319,36 @@ } db.aHook[db.nCommitHook].sequence = sequence; db.aHook[db.nCommitHook].xHook = x; db.nCommitHook++; } + +/* +** 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"); + } + if( g.db ) sqlite3_set_authorizer(g.db, xAuth, pArg); + 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); + } + if( g.db ) sqlite3_set_authorizer(g.db, 0, 0); + db.xAuth = 0; + db.pAuthArg = 0; +} #if INTERFACE /* ** Possible flags to db_vprepare */ @@ -844,34 +873,37 @@ 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); + if( db.xAuth ){ + sqlite3_set_authorizer(xdb, db.xAuth, db.pAuthArg); + } + 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 @@ -1793,11 +1825,11 @@ /* ** 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) +#if defined(_WIN32) || !defined(FOSSIL_LEGACY_ALLOW_SYMLINKS) return 0; #else return 1; #endif } @@ -2086,10 +2118,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); @@ -2115,17 +2148,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, @@ -2165,10 +2201,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; } @@ -2919,17 +2956,19 @@ dflt = 0; } fossil_free(zVal); return dflt; } +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS int db_get_versioned_boolean(const char *zName, int dflt){ char *zVal = db_get_versioned(zName, 0); if( zVal==0 ) return dflt; if( is_truth(zVal) ) return 1; if( is_false(zVal) ) return 0; return dflt; } +#endif /* FOSSIL_LEGACY_ALLOW_SYMLINKS */ char *db_lget(const char *zName, const char *zDefault){ return db_text(zDefault, "SELECT value FROM vvar WHERE name=%Q", zName); } void db_lset(const char *zName, const char *zValue){ @@ -3128,11 +3167,13 @@ void cmd_open(void){ int emptyFlag; int keepFlag; int forceMissingFlag; int allowNested; +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS int allowSymlinks; +#endif 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 */ @@ -3239,10 +3280,11 @@ }else if( db_exists("SELECT 1 FROM event WHERE type='ci'") ){ g.zOpenRevision = db_get("main-branch", 0); } } +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS 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, @@ -3252,10 +3294,11 @@ ** repository or global configuration databases only. */ allowSymlinks = db_get_versioned_boolean("allow-symlinks", -1); }else{ allowSymlinks = -1; /* Use non-versioned settings only. */ } +#endif #if defined(_WIN32) || defined(__CYGWIN__) # define LOCALDB_NAME "./_FOSSIL_" #else # define LOCALDB_NAME "./.fslckout" @@ -3265,10 +3308,11 @@ "COMMIT; PRAGMA journal_mode=WAL; BEGIN;", #endif (char*)0); db_delete_on_failure(LOCALDB_NAME); db_open_local(0); +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS 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 @@ -3281,10 +3325,11 @@ ** 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()); } +#endif /* FOSSIL_LEGACY_ALLOW_SYMLINKS */ db_lset("repository", zRepo); db_record_repository_filename(zRepo); db_set_checkout(0); azNewArgv[0] = g.argv[0]; g.argv = azNewArgv; @@ -3392,11 +3437,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) +#if !defined(FOSSIL_LEGACY_ALLOW_SYMLINKS) +/* +** SETTING: allow-symlinks boolean default=off +** +** When allow-symlinks is OFF (which is the default and recommended setting) +** symbolic links are treated like text files that contain a single line of +** content which is the name of their target. If allow-symlinks is ON, +** the symbolic links are actually followed. +** +** The use of symbolic links is dangerous. If you checkout a maliciously +** crafted checkin that contains symbolic links, it is possible that files +** outside of the working directory might be overwritten. +** +** Keep this setting OFF unless you have a very good reason to turn it +** on and you implicitly trust the integrity of the repositories you +** open. +*/ +#endif +#if defined(_WIN32) && defined(FOSSIL_LEGACY_ALLOW_SYMLINKS) /* ** 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, @@ -3403,11 +3466,11 @@ ** 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) +#if !defined(_WIN32) && defined(FOSSIL_LEGACY_ALLOW_SYMLINKS) /* ** 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, Index: src/file.c ================================================================== --- src/file.c +++ src/file.c @@ -323,10 +323,82 @@ ** 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 +642,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 ){ @@ -2397,5 +2472,88 @@ changeCount); }else{ fossil_print("Touched %d file(s)\n", changeCount); } } + +/* +** Returns non-zero if the specified file name ends with any reserved name, +** e.g.: _FOSSIL_ or .fslckout. Specifically, it returns 1 for exact match +** or 2 for a tail match on a longer file name. +** +** For the sake of efficiency, zFilename must be a canonical name, e.g. an +** absolute path using only forward slash ('/') as a directory separator. +** +** nFilename must be the length of zFilename. When negative, strlen() will +** be used to calculate it. +*/ +int file_is_reserved_name(const char *zFilename, int nFilename){ + const char *zEnd; /* one-after-the-end of zFilename */ + int gotSuffix = 0; /* length of suffix (-wal, -shm, -journal) */ + + assert( zFilename && "API misuse" ); + if( nFilename<0 ) nFilename = (int)strlen(zFilename); + if( nFilename<8 ) return 0; /* strlen("_FOSSIL_") */ + zEnd = zFilename + nFilename; + if( nFilename>=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; + } + } +} + +/* +** COMMAND: test-is-reserved-name +** +** Usage: %fossil test-is-ckout-db FILENAMES... +** +** Passes each given name to file_is_reserved_name() and outputs one +** line per file: the result value of that function followed by the +** name. +*/ +void test_is_reserved_name_cmd(void){ + int i; + + if(g.argc<3){ + usage("FILENAME_1 [...FILENAME_N]"); + } + for( i = 2; i < g.argc; ++i ){ + const int check = file_is_reserved_name(g.argv[i], -1); + fossil_print("%d %s\n", check, g.argv[i]); + } +} Index: src/http.c ================================================================== --- src/http.c +++ src/http.c @@ -375,17 +375,26 @@ j -= 4; zLine[j] = 0; } if( (mHttpFlags & HTTP_QUIET)==0 ){ fossil_print("redirect with status %d to %s\n", rc, &zLine[i]); + } + if( g.url.isFile || g.url.isSsh ){ + fossil_warning("cannot redirect from %s to %s", g.url.canonical, + &zLine[i]); + goto write_err; } wasHttps = g.url.isHttps; url_parse(&zLine[i], 0); if( wasHttps && !g.url.isHttps ){ fossil_warning("cannot redirect from HTTPS to HTTP"); goto write_err; - } + } + if( g.url.isSsh || g.url.isFile ){ + fossil_warning("cannot redirect to %s", &zLine[i]); + goto write_err; + } transport_close(&g.url); transport_global_shutdown(&g.url); fSeenHttpAuth = 0; if( g.zHttpAuth ) free(g.zHttpAuth); g.zHttpAuth = get_httpauth(); Index: src/json_config.c ================================================================== --- src/json_config.c +++ src/json_config.c @@ -83,11 +83,13 @@ { "keep-glob", CONFIGSET_PROJ }, { "crlf-glob", CONFIGSET_PROJ }, { "crnl-glob", CONFIGSET_PROJ }, { "encoding-glob", CONFIGSET_PROJ }, { "empty-dirs", CONFIGSET_PROJ }, +#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS { "allow-symlinks", CONFIGSET_PROJ }, +#endif { "dotfiles", CONFIGSET_PROJ }, { "ticket-table", CONFIGSET_TKT }, { "ticket-common", CONFIGSET_TKT }, { "ticket-change", CONFIGSET_TKT }, Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -1227,10 +1227,13 @@ #if defined(FOSSIL_DYNAMIC_BUILD) blob_append(pOut, "FOSSIL_DYNAMIC_BUILD\n", -1); #else blob_append(pOut, "FOSSIL_STATIC_BUILD\n", -1); #endif +#if defined(FOSSIL_LEGACY_ALLOW_SYMLINKS) + blob_append(pOut, "FOSSIL_LEGACY_ALLOW_SYMLINKS\n", -1); +#endif #if defined(HAVE_PLEDGE) blob_append(pOut, "HAVE_PLEDGE\n", -1); #endif #if defined(USE_MMAN_H) blob_append(pOut, "USE_MMAN_H\n", -1); 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/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/stash.c ================================================================== --- src/stash.c +++ src/stash.c @@ -334,10 +334,12 @@ blob_write_to_file(&delta, zNPath); file_setexe(zNPath, isExec); }else if( isRemoved ){ fossil_print("DELETE %s\n", zOrig); file_delete(zOPath); + }else if( file_unsafe_in_tree_path(zNPath) ){ + /* Ignore the unsafe path */ }else{ Blob a, b, out, disk; int isNewLink = file_islink(zOPath); db_ephemeral_blob(&q, 6, &delta); blob_read_from_file(&disk, zOPath, RepoFILE); Index: src/tkt.c ================================================================== --- src/tkt.c +++ src/tkt.c @@ -370,10 +370,79 @@ Th_FossilInit(TH_INIT_DEFAULT); Th_Store("uuid", zUuid); zConfig = ticket_change_code(); return Th_Eval(g.interp, 0, zConfig, -1); } + +/* +** An authorizer function for the SQL used to initialize the +** schema for the ticketing system. Only allow CREATE TABLE and +** CREATE INDEX for tables whose names begin with "ticket" and +** changes to tables whose names begin with "ticket". +*/ +static int ticket_schema_auth( + void *pNErr, + int eCode, + const char *z0, + const char *z1, + const char *z2, + const char *z3 +){ + switch( eCode ){ + case SQLITE_CREATE_TABLE: { + if( sqlite3_stricmp(z2,"main")!=0 + && sqlite3_stricmp(z2,"repository")!=0 + ){ + goto ticket_schema_error; + } + if( sqlite3_strnicmp(z0,"ticket",6)!=0 ){ + goto ticket_schema_error; + } + break; + } + case SQLITE_CREATE_INDEX: { + if( sqlite3_stricmp(z2,"main")!=0 + && sqlite3_stricmp(z2,"repository")!=0 + ){ + goto ticket_schema_error; + } + if( sqlite3_strnicmp(z1,"ticket",6)!=0 ){ + goto ticket_schema_error; + } + break; + } + case SQLITE_INSERT: + case SQLITE_UPDATE: + case SQLITE_DELETE: { + if( sqlite3_stricmp(z2,"main")!=0 + && sqlite3_stricmp(z2,"repository")!=0 + ){ + goto ticket_schema_error; + } + if( sqlite3_strnicmp(z0,"ticket",6)!=0 + && sqlite3_strnicmp(z0,"sqlite_",7)!=0 + ){ + goto ticket_schema_error; + } + break; + } + case SQLITE_REINDEX: + case SQLITE_TRANSACTION: + case SQLITE_READ: { + break; + } + default: { + goto ticket_schema_error; + } + } + return SQLITE_OK; + +ticket_schema_error: + if( pNErr ) *(int*)pNErr = 1; + return SQLITE_DENY; +} + /* ** Recreate the TICKET and TICKETCHNG tables. */ void ticket_create_table(int separateConnection){ @@ -384,14 +453,17 @@ "DROP TABLE IF EXISTS ticketchng;" ); zSql = ticket_table_schema(); if( separateConnection ){ if( db_transaction_nesting_depth() ) db_end_transaction(0); + db_set_authorizer(ticket_schema_auth,0,"Ticket-Schema"); db_init_database(g.zRepositoryName, zSql, 0); }else{ + db_set_authorizer(ticket_schema_auth,0,"Ticket-Schema"); db_multi_exec("%s", zSql/*safe-for-%s*/); } + db_clear_authorizer(); fossil_free(zSql); } /* ** Repopulate the TICKET and TICKETCHNG tables from scratch using all Index: src/undo.c ================================================================== --- src/undo.c +++ src/undo.c @@ -52,11 +52,11 @@ int new_exe; int new_link; int old_link; Blob current; Blob new; - zFullname = mprintf("%s/%s", g.zLocalRoot, zPathname); + zFullname = mprintf("%s%s", g.zLocalRoot, zPathname); old_link = db_column_int(&q, 3); new_exists = file_size(zFullname, RepoFILE)>=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/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",