Fossil

Check-in [c9b9a77d59]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add triggers to prevent changes to sensitive settings when PROTECT_SENSITIVE is engaged.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | sec2020
Files: files | file ages | folders
SHA3-256: c9b9a77d592f031aa279b2accacb64440b9e03d2c96461cacf9ae889ba7c5141
User & Date: drh 2020-08-21 13:04:08.831
Context
2020-08-21
15:05
Improved documentation of the database write protection logic. Added undocumented SQL command db_protect() and db_protect_pop() to the "sql" command. Panic on a protection stack overflow. check-in: 75deba73b5 user: drh tags: sec2020
13:04
Add triggers to prevent changes to sensitive settings when PROTECT_SENSITIVE is engaged. check-in: c9b9a77d59 user: drh tags: sec2020
11:26
Remove incorrect leaf ambiguity warning when doing a "fossil commit --dry-run". check-in: 1b52c41415 user: drh tags: sec2020
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/configure.c.
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460

461
462
463
464
465
466
467
       blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
    }
    blob_append_sql(&sql,") VALUES(%s,%s",
       azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
    for(jj=2; jj<nToken; jj+=2){
       blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
    }
    db_unprotect(PROTECT_ALL);
    db_multi_exec("%s)", blob_sql_text(&sql));
    db_protect_pop();
    if( db_changes()==0 ){
      blob_reset(&sql);
      blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
                      &zName[1], azToken[0]/*safe-for-%s*/);
      for(jj=2; jj<nToken; jj+=2){
        blob_append_sql(&sql, ", \"%w\"=%s",
                        azToken[jj], azToken[jj+1]/*safe-for-%s*/);
      }
      blob_append_sql(&sql, " WHERE \"%w\"=%s AND mtime<%s",
                   aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/,
                   azToken[0]/*safe-for-%s*/);
      db_unprotect(PROTECT_ALL);
      db_multi_exec("%s", blob_sql_text(&sql));
      db_protect_pop();
    }

    blob_reset(&sql);
    rebuildMask |= thisMask;
  }
}

/*
** Process a file full of "config" cards.







|

<











<

<

>







436
437
438
439
440
441
442
443
444

445
446
447
448
449
450
451
452
453
454
455

456

457
458
459
460
461
462
463
464
465
       blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
    }
    blob_append_sql(&sql,") VALUES(%s,%s",
       azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
    for(jj=2; jj<nToken; jj+=2){
       blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
    }
    db_protect_only(PROTECT_SENSITIVE);
    db_multi_exec("%s)", blob_sql_text(&sql));

    if( db_changes()==0 ){
      blob_reset(&sql);
      blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
                      &zName[1], azToken[0]/*safe-for-%s*/);
      for(jj=2; jj<nToken; jj+=2){
        blob_append_sql(&sql, ", \"%w\"=%s",
                        azToken[jj], azToken[jj+1]/*safe-for-%s*/);
      }
      blob_append_sql(&sql, " WHERE \"%w\"=%s AND mtime<%s",
                   aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/,
                   azToken[0]/*safe-for-%s*/);

      db_multi_exec("%s", blob_sql_text(&sql));

    }
    db_protect_pop();
    blob_reset(&sql);
    rebuildMask |= thisMask;
  }
}

/*
** Process a file full of "config" cards.
Changes to src/db.c.
133
134
135
136
137
138
139

140
141
142
143
144
145
146
  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 */

  int nProtect;             /* Slots of aProtect used */
  unsigned aProtect[10];    /* Saved values of protectMask */
} db = {
  PROTECT_USER|PROTECT_CONFIG,  /* protectMask */
  0, 0, 0, 0, 0, 0, };

/*







>







133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
  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 */
  int bProtectTriggers;     /* True if protection triggers already exist */
  int nProtect;             /* Slots of aProtect used */
  unsigned aProtect[10];    /* Saved values of protectMask */
} db = {
  PROTECT_USER|PROTECT_CONFIG,  /* protectMask */
  0, 0, 0, 0, 0, 0, };

/*
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
    if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n");
  }
  db.nBegin--;
  if( db.nBegin==0 ){
    int i;
    if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){
      i = 0;
      db_unprotect(PROTECT_ALL);
      while( db.nBeforeCommit ){
        db.nBeforeCommit--;
        sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0);
        sqlite3_free(db.azBeforeCommit[i]);
        i++;
      }
      leaf_do_pending_checks();







|







246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
    if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n");
  }
  db.nBegin--;
  if( db.nBegin==0 ){
    int i;
    if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){
      i = 0;
      db_protect_only(PROTECT_SENSITIVE);
      while( db.nBeforeCommit ){
        db.nBeforeCommit--;
        sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0);
        sqlite3_free(db.azBeforeCommit[i]);
        i++;
      }
      leaf_do_pending_checks();
328
329
330
331
332
333
334
335

336
337
338
339
340
341

342
343
344
345
346
347
348

349
350
351
352

353
354
355

356
357
358
359
360
361














362



363
364
365
366
367
368
369
370
371
372
373
374











375
376
377
378
379
380
381
  db.aHook[db.nCommitHook].sequence = sequence;
  db.aHook[db.nCommitHook].xHook = x;
  db.nCommitHook++;
}

#if INTERFACE
/*
** Flag bits for db_protect() and db_unprotect()

*/
#define PROTECT_USER       0x01
#define PROTECT_CONFIG     0x02
#define PROTECT_SENSITIVE  0x04
#define PROTECT_READONLY   0x08
#define PROTECT_ALL        0x0f  /* All of the above */

#endif /* INTERFACE */

/*
** Enable or disable database write protections.
** Use db_protect() to enable write permissions.  Use
** db_unprotect() to disable them.
**

** Each call to db_protect() and/or db_unprotect() should be followed
** by a corresponding call to db_protect_pop().  The db_protect_pop()
** call restores the protection settings to what they were before.
**

** The stack of protection settings is finite, so do not nest calls
** to db_protect()/db_unprotect() too deeply.  And make sure calls
** to db_protect()/db_unprotect() are balanced.

*/
void db_protect(unsigned flags){
  if( db.nProtect>=count(db.aProtect) ){
    fossil_fatal("too many db_protect() calls");
  }
  db.aProtect[db.nProtect++] = db.protectMask;














  db.protectMask |= flags;



}
void db_unprotect(unsigned flags){
  if( db.nProtect>=count(db.aProtect) ){
    fossil_fatal("too many db_unprotect() calls");
  }
  db.aProtect[db.nProtect++] = db.protectMask;
  db.protectMask &= ~flags;
}
void db_protect_pop(void){
  if( db.nProtect<1 ) fossil_fatal("too many db_protect_pop() calls");
  db.protectMask = db.aProtect[--db.nProtect];
}












/*
** 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().
*/







|
>

|
|
|
|

>




<
<

>
|
|
|

>
|
|
|
>

|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>












>
>
>
>
>
>
>
>
>
>
>







329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348


349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
  db.aHook[db.nCommitHook].sequence = sequence;
  db.aHook[db.nCommitHook].xHook = x;
  db.nCommitHook++;
}

#if INTERFACE
/*
** Flag bits for db_protect() and db_unprotect() indicating which parts
** of the databases should be write protected or write enabled, respectively.
*/
#define PROTECT_USER       0x01  /* USER table */
#define PROTECT_CONFIG     0x02  /* CONFIG and GLOBAL_CONFIG tables */
#define PROTECT_SENSITIVE  0x04  /* Sensitive and/or global settings */
#define PROTECT_READONLY   0x08  /* everything except TEMP tables */
#define PROTECT_ALL        0x0f  /* All of the above */
#define PROTECT_NONE       0x00  /* Nothing.  Everything is open */
#endif /* INTERFACE */

/*
** Enable or disable database write protections.


**
**    db_protext(X)         Add protects on X
**    db_unprotect(X)       Remove protections on X
**    db_protect_only(X)    Remove all prior protections then set
**                          protections to only X.
**
** Each of these routines pushes the previous protection mask onto
** a finite-size stack.  Each should be followed by a call to
** db_protect_pop() to pop the stack and restore the protections that
** existed prior to the call.  The protection mask stack has a limited
** depth, so take care not to next calls too deeply.
*/
void db_protect_only(unsigned flags){
  if( db.nProtect>=count(db.aProtect) ){
    fossil_fatal("too many db_protect() calls");
  }
  db.aProtect[db.nProtect++] = db.protectMask;
  if( (flags & PROTECT_SENSITIVE)!=0
   && (db.protectMask & PROTECT_SENSITIVE)==0
   && db.bProtectTriggers==0
  ){
    db_multi_exec(
      "CREATE TEMP TRIGGER IF NOT EXISTS protect_1"
      " BEFORE INSERT ON config WHEN protected_setting(new.name)"
      " BEGIN SELECT raise(abort,'not authorized'); END;\n"
      "CREATE TEMP TRIGGER IF NOT EXISTS 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) ){
    fossil_fatal("too many db_unprotect() calls");
  }
  db.aProtect[db.nProtect++] = db.protectMask;
  db.protectMask &= ~flags;
}
void db_protect_pop(void){
  if( db.nProtect<1 ) fossil_fatal("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_fatal("internal security assertion fault: missing "
                 "database protection bits: %02x", flags & ~db.protectMask);
  }
}

/*
** 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().
*/
394
395
396
397
398
399
400



401
402
403
404
405
406
407
    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_READONLY)!=0
                && sqlite3_stricmp(z2,"temp")!=0 ){
        rc = SQLITE_DENY;
      }
      break;
    }







>
>
>







426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
    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;
    }
2318
2319
2320
2321
2322
2323
2324

2325
2326
2327
2328
2329
2330
2331
        fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
      }
    }
    g.db = 0;
  }
  g.repositoryOpen = 0;
  g.localOpen = 0;

  assert( g.dbConfig==0 );
  assert( g.zConfigDbName==0 );
  backoffice_run_if_needed();
}

/*
** Close the database as quickly as possible without unnecessary processing.







>







2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
        fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
      }
    }
    g.db = 0;
  }
  g.repositoryOpen = 0;
  g.localOpen = 0;
  db.bProtectTriggers = 0;
  assert( g.dbConfig==0 );
  assert( g.zConfigDbName==0 );
  backoffice_run_if_needed();
}

/*
** Close the database as quickly as possible without unnecessary processing.
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
  );
  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|PROTECT_SENSITIVE);
    db_multi_exec(
       "DELETE FROM global_config WHERE name %s = %Q;",
       filename_collation(), zCkoutSetting
    );
    db_multi_exec(
      "REPLACE INTO global_config(name, value)"
      "VALUES(%Q,%Q);",







|







3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
  );
  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(
      "REPLACE INTO global_config(name, value)"
      "VALUES(%Q,%Q);",