Skip to content

Commit

Permalink
Prevent symlink attacks via .augnew during saving
Browse files Browse the repository at this point in the history
Instead of saving into a predictable PATH.augnew file, save into a securely
created PATH.augnew.XXXXXX

* src/transform.c (transform_save):
    write changes to a temporary file in the same directory as the destination
    (either the file's canonical path or the path of .augnew), before renaming

* src/transform.c (transfer_file_attrs):
    use fchown, fchmod etc. on the same file handles to ensure consistent
    permission changes

* bootstrap: add mkstemp gnulib module
* tests/
    test-put-symlink-augnew.sh: test symlink attack when writing .augnew
    test-put-symlink-augsave.sh: test symlink attack when writing .augsave
    test-put-symlink-augtemp.sh: test symlink attack via temp .augnew
    test-put-symlink.sh: also test file modification

Fixes BZ 772257
  • Loading branch information
Dominic Cleal authored and David Lutterkort committed Jul 19, 2012
1 parent 730cdda commit 1638774
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 59 deletions.
1 change: 1 addition & 0 deletions bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ gitlog-to-changelog
canonicalize-lgpl
isblank
locale
mkstemp
regex
safe-alloc
selinux-h
Expand Down
15 changes: 12 additions & 3 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,14 @@ fread_file_lim (FILE *stream, size_t max_len, size_t *length)
return NULL;
}

char* xread_file(const char *path) {
FILE *fp = fopen(path, "r");
char* xfread_file(FILE *fp) {
char *result;
size_t len;

if (!fp)
return NULL;

result = fread_file_lim(fp, MAX_READ_LEN, &len);
fclose (fp);

if (result != NULL
&& len <= MAX_READ_LEN
Expand All @@ -145,6 +143,17 @@ char* xread_file(const char *path) {
return NULL;
}

char* xread_file(const char *path) {
FILE *fp;
char *result;

fp = fopen(path, "r");
result = xfread_file(fp);
fclose (fp);

return result;
}

/*
* Escape/unescape of string literals
*/
Expand Down
3 changes: 3 additions & 0 deletions src/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ char *format_pos(const char *text, int pos);
*/
char* xread_file(const char *path);

/* Like xread_file, but caller supplies a file pointer */
char* xfread_file(FILE *fp);

/* Get the error message for ERRNUM in a threadsafe way. Based on libvirt's
* virStrError
*/
Expand Down
141 changes: 88 additions & 53 deletions src/transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -799,35 +799,38 @@ int transform_applies(struct tree *xfm, const char *path) {
return filter_matches(xfm, path + strlen(AUGEAS_FILES_TREE));
}

static int transfer_file_attrs(const char *from, const char *to,
static int transfer_file_attrs(FILE *from, FILE *to,
const char **err_status) {
struct stat st;
int ret = 0;
int selinux_enabled = (is_selinux_enabled() > 0);
security_context_t con = NULL;

ret = lstat(from, &st);
int from_fd = fileno(from);
int to_fd = fileno(to);

ret = fstat(from_fd, &st);
if (ret < 0) {
*err_status = "replace_stat";
return -1;
}
if (selinux_enabled) {
if (lgetfilecon(from, &con) < 0 && errno != ENOTSUP) {
if (fgetfilecon(from_fd, &con) < 0 && errno != ENOTSUP) {
*err_status = "replace_getfilecon";
return -1;
}
}

if (lchown(to, st.st_uid, st.st_gid) < 0) {
if (fchown(to_fd, st.st_uid, st.st_gid) < 0) {
*err_status = "replace_chown";
return -1;
}
if (chmod(to, st.st_mode) < 0) {
if (fchmod(to_fd, st.st_mode) < 0) {
*err_status = "replace_chmod";
return -1;
}
if (selinux_enabled && con != NULL) {
if (lsetfilecon(to, con) < 0 && errno != ENOTSUP) {
if (fsetfilecon(to_fd, con) < 0 && errno != ENOTSUP) {
*err_status = "replace_setfilecon";
return -1;
}
Expand Down Expand Up @@ -869,7 +872,7 @@ static int clone_file(const char *from, const char *to,
goto done;
}

if (transfer_file_attrs(from, to, err_status) < 0)
if (transfer_file_attrs(from_fp, to_fp, err_status) < 0)
goto done;

while ((len = fread(buf, 1, BUFSIZ, from_fp)) > 0) {
Expand Down Expand Up @@ -946,19 +949,38 @@ static int file_saved_event(struct augeas *aug, const char *path) {
* are noted in the /augeas/files hierarchy in AUG->ORIGIN under
* PATH/error.
*
* Writing the file happens by first writing into PATH.augnew, transferring
* all file attributes of PATH to PATH.augnew, and then renaming
* PATH.augnew to PATH. If the rename fails, and the entry
* AUGEAS_COPY_IF_FAILURE exists in AUG->ORIGIN, PATH is overwritten by
* copying file contents
* Writing the file happens by first writing into a temp file, transferring all
* file attributes of PATH to the temp file, and then renaming the temp file
* back to PATH.
*
* Temp files are created alongside the destination file to enable the rename,
* which may be the canonical path (PATH_canon) if PATH is a symlink.
*
* If the AUG_SAVE_NEWFILE flag is set, instead rename to PATH.augnew rather
* than PATH. If AUG_SAVE_BACKUP is set, move the original to PATH.augsave.
* (Always PATH.aug{new,save} irrespective of whether PATH is a symlink.)
*
* If the rename fails, and the entry AUGEAS_COPY_IF_FAILURE exists in
* AUG->ORIGIN, PATH is instead overwritten by copying file contents.
*
* The table below shows the locations for each permutation.
*
* PATH save flag temp file dest file backup?
* regular - PATH.augnew.XXXX PATH -
* regular BACKUP PATH.augnew.XXXX PATH PATH.augsave
* regular NEWFILE PATH.augnew.XXXX PATH.augnew -
* symlink - PATH_canon.XXXX PATH_canon -
* symlink BACKUP PATH_canon.XXXX PATH_canon PATH.augsave
* symlink NEWFILE PATH.augnew.XXXX PATH.augnew -
*
* Return 0 on success, -1 on failure.
*/
int transform_save(struct augeas *aug, struct tree *xfm,
const char *path, struct tree *tree) {
FILE *fp = NULL;
char *augnew = NULL, *augorig = NULL, *augsave = NULL;
char *augorig_canon = NULL;
int fd;
FILE *fp = NULL, *augorig_canon_fp = NULL;
char *augtemp = NULL, *augnew = NULL, *augorig = NULL, *augsave = NULL;
char *augorig_canon = NULL, *augdest = NULL;
int augorig_exists;
int copy_if_rename_fails = 0;
char *text = NULL;
Expand Down Expand Up @@ -986,19 +1008,6 @@ int transform_save(struct augeas *aug, struct tree *xfm,
goto done;
}

if (access(augorig, R_OK) == 0) {
text = xread_file(augorig);
} else {
text = strdup("");
}

if (text == NULL) {
err_status = "put_read";
goto done;
}

text = append_newline(text, strlen(text));

augorig_canon = canonicalize_file_name(augorig);
augorig_exists = 1;
if (augorig_canon == NULL) {
Expand All @@ -1011,31 +1020,53 @@ int transform_save(struct augeas *aug, struct tree *xfm,
}
}

/* Figure out where to put the .augnew file. If we need to rename it
later on, put it next to augorig_canon */
if (access(augorig_canon, R_OK) == 0) {
augorig_canon_fp = fopen(augorig_canon, "r");
text = xfread_file(augorig_canon_fp);
} else {
text = strdup("");
}

if (text == NULL) {
err_status = "put_read";
goto done;
}

text = append_newline(text, strlen(text));

/* Figure out where to put the .augnew and temp file. If no .augnew file
then put the temp file next to augorig_canon, else next to .augnew. */
if (aug->flags & AUG_SAVE_NEWFILE) {
if (xasprintf(&augnew, "%s" EXT_AUGNEW, augorig) < 0) {
err_status = "augnew_oom";
goto done;
}
augdest = augnew;
} else {
if (xasprintf(&augnew, "%s" EXT_AUGNEW, augorig_canon) < 0) {
err_status = "augnew_oom";
goto done;
}
augdest = augorig_canon;
}

if (xasprintf(&augtemp, "%s.XXXXXX", augdest) < 0) {
err_status = "augtemp_oom";
goto done;
}

// FIXME: We might have to create intermediate directories
// to be able to write augnew, but we have no idea what permissions
// etc. they should get. Just the process default ?
fp = fopen(augnew, "w");
fd = mkstemp(augtemp);
if (fd < 0) {
err_status = "mk_augtemp";
goto done;
}
fp = fdopen(fd, "w");
if (fp == NULL) {
err_status = "open_augnew";
err_status = "open_augtemp";
goto done;
}

if (augorig_exists) {
if (transfer_file_attrs(augorig_canon, augnew, &err_status) != 0) {
if (transfer_file_attrs(augorig_canon_fp, fp, &err_status) != 0) {
err_status = "xfer_attrs";
goto done;
}
Expand All @@ -1045,22 +1076,22 @@ int transform_save(struct augeas *aug, struct tree *xfm,
lns_put(fp, lens, tree->children, text, &err);

if (ferror(fp)) {
err_status = "error_augnew";
err_status = "error_augtemp";
goto done;
}

if (fflush(fp) != 0) {
err_status = "flush_augnew";
err_status = "flush_augtemp";
goto done;
}

if (fsync(fileno(fp)) < 0) {
err_status = "sync_augnew";
err_status = "sync_augtemp";
goto done;
}

if (fclose(fp) != 0) {
err_status = "close_augnew";
err_status = "close_augtemp";
fp = NULL;
goto done;
}
Expand All @@ -1069,33 +1100,33 @@ int transform_save(struct augeas *aug, struct tree *xfm,

if (err != NULL) {
err_status = err->pos >= 0 ? "parse_skel_failed" : "put_failed";
unlink(augnew);
unlink(augtemp);
goto done;
}

{
char *new_text = xread_file(augnew);
char *new_text = xread_file(augtemp);
int same = 0;
if (new_text == NULL) {
err_status = "read_augnew";
err_status = "read_augtemp";
goto done;
}
same = STREQ(text, new_text);
FREE(new_text);
if (same) {
result = 0;
unlink(augnew);
unlink(augtemp);
goto done;
} else if (aug->flags & AUG_SAVE_NOOP) {
result = 1;
unlink(augnew);
unlink(augtemp);
goto done;
}
}

if (!(aug->flags & AUG_SAVE_NEWFILE)) {
if (augorig_exists && (aug->flags & AUG_SAVE_BACKUP)) {
r = asprintf(&augsave, "%s%s" EXT_AUGSAVE, aug->root, filename);
r = xasprintf(&augsave, "%s" EXT_AUGSAVE, augorig);
if (r == -1) {
augsave = NULL;
goto done;
Expand All @@ -1107,13 +1138,14 @@ int transform_save(struct augeas *aug, struct tree *xfm,
goto done;
}
}
r = clone_file(augnew, augorig_canon, &err_status,
copy_if_rename_fails);
if (r != 0) {
dyn_err_status = strappend(err_status, "_augnew");
goto done;
}
}

r = clone_file(augtemp, augdest, &err_status, copy_if_rename_fails);
if (r != 0) {
dyn_err_status = strappend(err_status, "_augtemp");
goto done;
}

result = 1;

done:
Expand All @@ -1138,6 +1170,7 @@ int transform_save(struct augeas *aug, struct tree *xfm,
free(dyn_err_status);
lens_release(lens);
free(text);
free(augtemp);
free(augnew);
if (augorig_canon != augorig)
free(augorig_canon);
Expand All @@ -1147,6 +1180,8 @@ int transform_save(struct augeas *aug, struct tree *xfm,

if (fp != NULL)
fclose(fp);
if (augorig_canon_fp != NULL)
fclose(augorig_canon_fp);
return result;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ check_SCRIPTS = \
test-interpreter.sh \
$(lens_tests) \
test-get.sh test-augtool.sh \
test-put-symlink.sh test-save-empty.sh \
test-put-symlink.sh test-put-symlink-augnew.sh \
test-put-symlink-augsave.sh test-put-symlink-augtemp.sh test-save-empty.sh \
test-bug-1.sh test-idempotent.sh test-preserve.sh \
test-events-saved.sh test-save-mode.sh test-unlink-error.sh \
test-augtool-empty-line.sh test-augtool-modify-root.sh
Expand Down
Loading

0 comments on commit 1638774

Please sign in to comment.