Commit message: comics: Remove support for tar and tar-like commands

When handling tar files, or using a command with tar-compatible syntax,
to open comic-book archives, both the archive name (the name of the
comics file) and the filename (the name of a page within the archive)
are quoted to not be interpreted by the shell.

But the filename is completely with the attacker's control and can start
with "--" which leads to tar interpreting it as a command line flag.

This can be exploited by creating a CBT file (a tar archive with the
.cbt suffix) with an embedded file named something like this:
"--checkpoint-action=exec=bash -c 'touch ~/hacked;'.jpg"

CBT files are infinitely rare (CBZ is usually used for DRM-free
commercial releases, CBR for those from more dubious provenance), so
removing support is the easiest way to avoid the bug triggering. All
this code was rewritten in the development release for GNOME 3.26 to not
shell out to any command, closing off this particular attack vector.

This also removes the ability to use libarchive's bsdtar-compatible
binary for CBZ (ZIP), CB7 (7zip), and CBR (RAR) formats. The first two
are already supported by unzip and 7zip respectively. libarchive's RAR
support is limited, so unrar is a requirement anyway.

Discovered by Felix Wilhelm from the Google Security Team.

https://bugzilla.gnome.org/show_bug.cgi?id=784630
CVE: CVE-2017-1000083
CWE: NVD-CWE-noinfo
Vulnerability description: backend/comics/comics-document.c (aka the comic book backend) in GNOME Evince before 3.24.1 allows remote attackers to execute arbitrary commands via a .cbt file that is a TAR archive containing a filename beginning with a "--" command-line option substring, as demonstrated by a --checkpoint-action=exec=bash at the beginning of the filename.

Project: evince
File: comics-document.c

comics_decompress_temp_dir (const gchar *command_decompress_tmp,
			    const gchar *command, 
			    GError      **error)
{
	gboolean success;
	gchar *std_out, *basename;
	GError *err = NULL;
	gint retval;
	
	success = g_spawn_command_line_sync (command_decompress_tmp, &std_out, 
					     NULL, &retval, &err);
	basename = g_path_get_basename (command);
	if (!success) {
		g_set_error (error,
			     EV_DOCUMENT_ERROR, 
			     EV_DOCUMENT_ERROR_INVALID,
			     _("Error launching the command “%s” in order to "
			     "decompress the comic book: %s"),
			     basename,
			     err->message);
		g_error_free (err);
	} else if (WIFEXITED (retval)) {
		if (WEXITSTATUS (retval) == EXIT_SUCCESS) {
			g_free (std_out);
			g_free (basename);
			return TRUE;
		} else {
			g_set_error (error,
				     EV_DOCUMENT_ERROR,
				     EV_DOCUMENT_ERROR_INVALID,
				     _("The command “%s” failed at "
				     "decompressing the comic book."),
				     basename);
			g_free (std_out);
		}
	} else {
		g_set_error (error,
			     EV_DOCUMENT_ERROR,
			     EV_DOCUMENT_ERROR_INVALID,
			     _("The command “%s” did not end normally."),
			     basename);
		g_free (std_out);
	}
	g_free (basename);
	return FALSE;
}

Project: evince
File: comics-document.c

comics_document_class_init (ComicsDocumentClass *klass)
{
	GObjectClass    *gobject_class = G_OBJECT_CLASS (klass);
	EvDocumentClass *ev_document_class = EV_DOCUMENT_CLASS (klass);

	gobject_class->finalize = comics_document_finalize;

	ev_document_class->load = comics_document_load;
	ev_document_class->save = comics_document_save;
	ev_document_class->get_n_pages = comics_document_get_n_pages;
	ev_document_class->get_page_size = comics_document_get_page_size;
	ev_document_class->render = comics_document_render;
}

Project: evince
File: comics-document.c

comics_document_init (ComicsDocument *comics_document)
{
	comics_document->archive = NULL;
	comics_document->page_names = NULL;
	comics_document->extract_command = NULL;
}

Project: evince
File: comics-document.c

comics_document_load (EvDocument *document,
		      const char *uri,
		      GError    **error)
{
	ComicsDocument *comics_document = COMICS_DOCUMENT (document);
	GSList *supported_extensions;
	gchar *std_out;
	gchar *mime_type;
	gchar **cb_files, *cb_file;
	gboolean success;
	int i, retval;
	GError *err = NULL;

	comics_document->archive = g_filename_from_uri (uri, NULL, error);
	if (!comics_document->archive)
		return FALSE;

	mime_type = ev_file_get_mime_type (uri, FALSE, &err);
	if (mime_type == NULL)
		return FALSE;
	
	if (!comics_check_decompress_command (mime_type, comics_document, 
	error)) {	
		g_free (mime_type);
		return FALSE;
	} else if (!comics_generate_command_lines (comics_document, error)) {
		   g_free (mime_type);
		return FALSE;
	}

	g_free (mime_type);

	/* Get list of files in archive */
	success = g_spawn_command_line_sync (comics_document->list_command,
					     &std_out, NULL, &retval, error);

	if (!success) {
		return FALSE;
	} else if (!WIFEXITED(retval) || WEXITSTATUS(retval) != EXIT_SUCCESS) {
		g_set_error_literal (error,
                                     EV_DOCUMENT_ERROR,
                                     EV_DOCUMENT_ERROR_INVALID,
                                     _("File corrupted"));
		return FALSE;
	}

	/* FIXME: is this safe against filenames containing \n in the archive ? */
	cb_files = g_strsplit (std_out, EV_EOL, 0);

	g_free (std_out);

	if (!cb_files) {
		g_set_error_literal (error,
				     EV_DOCUMENT_ERROR,
				     EV_DOCUMENT_ERROR_INVALID,
				     _("No files in archive"));
		return FALSE;
	}

        comics_document->page_names = g_ptr_array_sized_new (64);

	supported_extensions = get_supported_image_extensions ();
	for (i = 0; cb_files[i] != NULL; i++) {
		if (comics_document->offset != NO_OFFSET) {
			if (g_utf8_strlen (cb_files[i],-1) > 
			    comics_document->offset) {
				cb_file = 
					g_utf8_offset_to_pointer (cb_files[i], 
						       comics_document->offset);
			} else {
				continue;
			}
		} else {
			cb_file = cb_files[i];
		}
		gchar *suffix = g_strrstr (cb_file, ".");
		if (!suffix)
			continue;
		suffix = g_ascii_strdown (suffix + 1, -1);
		if (g_slist_find_custom (supported_extensions, suffix,
					 (GCompareFunc) strcmp) != NULL) {
                        g_ptr_array_add (comics_document->page_names,
                                         g_strstrip (g_strdup (cb_file)));
		}
		g_free (suffix);
	}
	g_strfreev (cb_files);
	g_slist_foreach (supported_extensions, (GFunc) g_free, NULL);
	g_slist_free (supported_extensions);

	if (comics_document->page_names->len == 0) {
		g_set_error (error,
			     EV_DOCUMENT_ERROR,
			     EV_DOCUMENT_ERROR_INVALID,
			     _("No images found in archive %s"),
			     uri);
		return FALSE;
	}

        /* Now sort the pages */
        g_ptr_array_sort (comics_document->page_names, sort_page_names);

	return TRUE;
}

Project: evince
File: comics-document.c

comics_document_render (EvDocument      *document,
			EvRenderContext *rc)
{
	GdkPixbuf       *pixbuf;
	cairo_surface_t *surface;

	pixbuf = comics_document_render_pixbuf (document, rc);
	surface = ev_document_misc_surface_from_pixbuf (pixbuf);
	g_object_unref (pixbuf);
	
	return surface;
}

Project: evince
File: comics-document.c

comics_document_render_pixbuf (EvDocument      *document,
			       EvRenderContext *rc)
{
	GdkPixbufLoader *loader;
	GdkPixbuf *rotated_pixbuf, *tmp_pixbuf;
	char **argv;
	guchar buf[4096];
	gboolean success;
	gint outpipe = -1;
	GPid child_pid;
	gssize bytes;
	gint width, height;
	gchar *filename;
	ComicsDocument *comics_document = COMICS_DOCUMENT (document);
	
	if (!comics_document->decompress_tmp) {
		argv = extract_argv (document, rc->page->index);
		success = g_spawn_async_with_pipes (NULL, argv, NULL,
						    G_SPAWN_SEARCH_PATH | 
						    G_SPAWN_STDERR_TO_DEV_NULL,
						    NULL, NULL,
						    &child_pid,
						    NULL, &outpipe, NULL, NULL);
		g_strfreev (argv);
		g_return_val_if_fail (success == TRUE, NULL);

		loader = gdk_pixbuf_loader_new ();
		g_signal_connect (loader, "size-prepared",
				  G_CALLBACK (render_pixbuf_size_prepared_cb), 
				  rc);

		while (outpipe >= 0) {
			bytes = read (outpipe, buf, 4096);

			if (bytes > 0) {
				gdk_pixbuf_loader_write (loader, buf, bytes, 
				NULL);
			} else if (bytes <= 0) {
				close (outpipe);
				gdk_pixbuf_loader_close (loader, NULL);
				outpipe = -1;
			}
		}
		tmp_pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
		rotated_pixbuf =
			gdk_pixbuf_rotate_simple (tmp_pixbuf,
						  360 - rc->rotation);
		g_spawn_close_pid (child_pid);
		g_object_unref (loader);
	} else {
		int scaled_width, scaled_height;

		filename = 
			g_build_filename (comics_document->dir,
                                          (char *) comics_document->page_names->pdata[rc->page->index],
					  NULL);
	   
		gdk_pixbuf_get_file_info (filename, &width, &height);

		ev_render_context_compute_scaled_size (rc, width, height,
						       &scaled_width, &scaled_height);
		
		tmp_pixbuf =
			gdk_pixbuf_new_from_file_at_size (
				    filename, scaled_width, scaled_height, NULL);
		rotated_pixbuf =
			gdk_pixbuf_rotate_simple (tmp_pixbuf,
						  360 - rc->rotation);
		g_free (filename);
		g_object_unref (tmp_pixbuf);
	}
	return rotated_pixbuf;
}

Project: evince
File: comics-document.c

comics_document_save (EvDocument *document,
		      const char *uri,
		      GError    **error)
{
	ComicsDocument *comics_document = COMICS_DOCUMENT (document);

	return ev_xfer_uri_simple (comics_document->archive, uri, error);
}

Project: evince
File: comics-document.c

comics_generate_command_lines (ComicsDocument *comics_document, 
			       GError         **error)
{
	gchar *quoted_file, *quoted_file_aux;
	gchar *quoted_command;
	ComicBookDecompressType type;
	
	type = comics_document->command_usage;
	comics_document->regex_arg = command_usage_def[type].regex_arg;
	quoted_command = g_shell_quote (comics_document->selected_command);
	if (comics_document->regex_arg) {
		quoted_file = comics_regex_quote (comics_document->archive);
		quoted_file_aux = g_shell_quote (comics_document->archive);
		comics_document->list_command =
			   g_strdup_printf (command_usage_def[type].list,
			                    comics_document->alternative_command,
			                    quoted_file_aux);
		g_free (quoted_file_aux);
	} else {
		quoted_file = g_shell_quote (comics_document->archive);
		comics_document->list_command =
				g_strdup_printf (command_usage_def[type].list,
				                 quoted_command, quoted_file);
	}
	comics_document->extract_command =
			    g_strdup_printf (command_usage_def[type].extract,
				             quoted_command);
	comics_document->offset = command_usage_def[type].offset;
	if (command_usage_def[type].decompress_tmp) {
		comics_document->dir = ev_mkdtemp ("evince-comics-XXXXXX", error);
                if (comics_document->dir == NULL)
                        return FALSE;

		/* unrar-free can't create directories, but ev_mkdtemp already created the dir */

		comics_document->decompress_tmp =
			g_strdup_printf (command_usage_def[type].decompress_tmp, 
					 quoted_command, quoted_file,
					 comics_document->dir);
		g_free (quoted_file);
		g_free (quoted_command);

		if (!comics_decompress_temp_dir (comics_document->decompress_tmp,
		    comics_document->selected_command, error))
			return FALSE;
		else
			return TRUE;
	} else {
		g_free (quoted_file);
		g_free (quoted_command);
		return TRUE;
	}

}

Project: evince
File: comics-document.c

comics_regex_quote (const gchar *unquoted_string)
{
	const gchar *p;
	GString *dest;

	dest = g_string_new ("'");

	p = unquoted_string;

	while (*p) {
		switch (*p) {
			/* * matches a sequence of 0 or more characters */
			case ('*'):
			/* ? matches exactly 1 charactere */
			case ('?'):
			/* [...]  matches any single character found inside
			 * the brackets. Disabling the first bracket is enough.
			 */
			case ('['):
				g_string_append (dest, "[");
				g_string_append_c (dest, *p);
				g_string_append (dest, "]");
				break;
			/* Because \ escapes regex expressions that we are
			 * disabling for unzip, we need to disable \ too */
			case ('\\'):
				g_string_append (dest, "[\\\\]");
				break;
			/* Escape single quote inside the string */
			case ('\''):
				g_string_append (dest, "'\\''");
				break;
			default:
				g_string_append_c (dest, *p);
				break;
		}
		++p;
	}
	g_string_append_c (dest, '\'');
	return g_string_free (dest, FALSE);
}

Project: evince
File: comics-document.c

comics_remove_dir (gchar *path_name) 
{
	GDir  *content_dir;
	const gchar *filename;
	gchar *filename_with_path;
	
	if (g_file_test (path_name, G_FILE_TEST_IS_DIR)) {
		content_dir = g_dir_open  (path_name, 0, NULL);
		filename  = g_dir_read_name (content_dir);
		while (filename) {
			filename_with_path = 
				g_build_filename (path_name, 
						  filename, NULL);
			comics_remove_dir (filename_with_path);
			g_free (filename_with_path);
			filename = g_dir_read_name (content_dir);
		}
		g_dir_close (content_dir);
	}
	/* Note from g_remove() documentation: on Windows, it is in general not 
	 * possible to remove a file that is open to some process, or mapped 
	 * into memory.*/
	return (g_remove (path_name));
}

Project: evince
File: comics-document.c

extract_argv (EvDocument *document, gint page)
{
	ComicsDocument *comics_document = COMICS_DOCUMENT (document);
	char **argv;
	char *command_line, *quoted_archive, *quoted_filename;
	GError *err = NULL;

        if (page >= comics_document->page_names->len)
                return NULL;

	if (comics_document->regex_arg) {
		quoted_archive = g_shell_quote (comics_document->archive);
		quoted_filename =
			comics_regex_quote (comics_document->page_names->pdata[page]);
	} else {
		quoted_archive = g_shell_quote (comics_document->archive);
		quoted_filename = g_shell_quote (comics_document->page_names->pdata[page]);
	}

	command_line = g_strdup_printf ("%s %s %s",
					comics_document->extract_command,
					quoted_archive,
					quoted_filename);
	g_shell_parse_argv (command_line, NULL, &argv, &err);

	if (err) {
		g_warning (_("Error %s"), err->message);
		g_error_free (err);
		return NULL;
	}

	g_free (command_line);
	g_free (quoted_archive);
	g_free (quoted_filename);
	return argv;
}

Project: evince
File: comics-document.c

get_page_size_area_prepared_cb (GdkPixbufLoader *loader,
				gpointer         data)
{
	gboolean *got_size = data;
	*got_size = TRUE;
}

Project: evince
File: comics-document.c

get_supported_image_extensions(void)
{
	GSList *extensions = NULL;
	GSList *formats = gdk_pixbuf_get_formats ();
	GSList *l;

	for (l = formats; l != NULL; l = l->next) {
		int i;
		gchar **ext = gdk_pixbuf_format_get_extensions (l->data);

		for (i = 0; ext[i] != NULL; i++) {
			extensions = g_slist_append (extensions,
						     g_strdup (ext[i]));
		}

		g_strfreev (ext);
	}

	g_slist_free (formats);
	return extensions;
}

Project: evince
File: comics-document.c

render_pixbuf_size_prepared_cb (GdkPixbufLoader *loader,
				gint             width,
				gint             height,
				EvRenderContext *rc)
{
	int scaled_width, scaled_height;

	ev_render_context_compute_scaled_size (rc, width, height, &scaled_width, &scaled_height);
	gdk_pixbuf_loader_set_size (loader, scaled_width, scaled_height);
}