Commit message: Work around shell code in podcast names (#598)
CVE: CVE-2017-14500
CWE: CWE-78
Vulnerability description: Improper Neutralization of Special Elements used in an OS Command in the podcast playback function of Podbeuter in Newsbeuter 0.3 through 2.9 allows remote attackers to perform user-assisted code execution by crafting an RSS item with a media enclosure (i.e., a podcast file) that includes shell metacharacters in its filename, related to pb_controller.cpp and queueloader.cpp, a different vulnerability than CVE-2017-12904.

Project: newsbeuter
File: pb_controller.cpp

void pb_controller::start_downloads() {
	int dl2start = get_maxdownloads() - downloads_in_progress();
	for (auto it=downloads_.begin(); dl2start > 0 && it!=downloads_.end(); ++it) {
		if (it->status() == DL_QUEUED) {
			std::thread t {poddlthread(&(*it), cfg)};
			--dl2start;
		}
	}
}

Project: newsbeuter
File: pb_controller.cpp

double pb_controller::get_total_kbps() {
	double result = 0.0;
	for (auto dl : downloads_) {
		if (dl.status() == DL_DOWNLOADING) {
			result += dl.kbps();
		}
	}
	return result;
}

Project: newsbeuter
File: pb_controller.cpp

void pb_controller::usage(const char * argv0) {
	std::cout << utils::strprintf(_("%s %s\nusage %s [-C <file>] [-q <file>] [-h]\n"
	                                "-C <configfile> read configuration from <configfile>\n"
	                                "-q <queuefile>  use <queuefile> as queue file\n"
	                                "-a              start download on startup\n"
	                                "-h              this help\n"), "podbeuter", PROGRAM_VERSION, argv0);
	::exit(EXIT_FAILURE);
}

Project: newsbeuter
File: pb_controller.cpp

void pb_controller::run(int argc, char * argv[]) {
	int c;
	bool automatic_dl = false;

	::signal(SIGINT, ctrl_c_action);

	do {
		if ((c = ::getopt(argc, argv, "C:q:d:l:ha")) < 0)
			continue;

		switch (c) {
		case ':':
		case '?':
			usage(argv[0]);
			break;
		case 'C':
			config_file = optarg;
			break;
		case 'q':
			queue_file = optarg;
			break;
		case 'a':
			automatic_dl = true;
			break;
		case 'd': // this is an undocumented debug commandline option!
			logger::getInstance().set_logfile(optarg);
			break;
		case 'l': { // this is an undocumented debug commandline option!
			loglevel level = static_cast<loglevel>(atoi(optarg));
			if (level > LOG_NONE && level <= LOG_DEBUG)
				logger::getInstance().set_loglevel(level);
		}
		break;
		case 'h':
			usage(argv[0]);
			break;
		default:
			std::cout << utils::strprintf(_("%s: unknown option - %c"), argv[0], static_cast<char>(c)) << std::endl;
			usage(argv[0]);
			break;
		}
	} while (c != -1);

	std::cout << utils::strprintf(_("Starting %s %s..."), "podbeuter", PROGRAM_VERSION) << std::endl;

	pid_t pid;
	if (!utils::try_fs_lock(lock_file, pid)) {
		std::cout << utils::strprintf(_("Error: an instance of %s is already running (PID: %u)"), "podbeuter", pid) << std::endl;
		return;
	}

	std::cout << _("Loading configuration...");
	std::cout.flush();

	configparser cfgparser;
	cfg = new configcontainer();
	cfg->register_commands(cfgparser);
	colormanager * colorman = new colormanager();
	colorman->register_commands(cfgparser);

	keymap keys(KM_PODBEUTER);
	cfgparser.register_handler("bind-key", &keys);
	cfgparser.register_handler("unbind-key", &keys);

	null_config_action_handler null_cah;
	cfgparser.register_handler("macro", &null_cah);
	cfgparser.register_handler("ignore-article", &null_cah);
	cfgparser.register_handler("always-download", &null_cah);
	cfgparser.register_handler("define-filter", &null_cah);
	cfgparser.register_handler("highlight", &null_cah);
	cfgparser.register_handler("highlight-article", &null_cah);
	cfgparser.register_handler("reset-unread-on-update", &null_cah);

	try {
		cfgparser.parse("/etc/newsbeuter/config");
		cfgparser.parse(config_file);
	} catch (const configexception& ex) {
		std::cout << ex.what() << std::endl;
		delete colorman;
		return;
	}

	if (colorman->colors_loaded())
		colorman->set_pb_colors(v);
	delete colorman;

	max_dls = cfg->get_configvalue_as_int("max-downloads");

	std::cout << _("done.") << std::endl;

	ql = new queueloader(queue_file, this);
	ql->reload(downloads_);

	v->set_keymap(&keys);

	v->run(automatic_dl);

	stfl::reset();

	std::cout <<  _("Cleaning up queue...");
	std::cout.flush();

	ql->reload(downloads_);
	delete ql;

	std::cout << _("done.") << std::endl;

	utils::remove_fs_lock(lock_file);
}

Project: newsbeuter
File: pb_controller.cpp

static void ctrl_c_action(int sig) {
	LOG(LOG_DEBUG,"caugh signal %d",sig);
	stfl::reset();
	utils::remove_fs_lock(lock_file);
	::exit(EXIT_FAILURE);
}

Project: newsbeuter
File: pb_controller.cpp

unsigned int pb_controller::downloads_in_progress() {
	unsigned int count = 0;
	for (auto dl : downloads_) {
		if (dl.status() == DL_DOWNLOADING)
			++count;
	}
	return count;
}

Project: newsbeuter
File: queueloader.cpp

std::string queueloader::get_filename(const std::string& str) {
	std::string fn = ctrl->get_dlpath();

	if (fn[fn.length()-1] != NEWSBEUTER_PATH_SEP[0])
		fn.append(NEWSBEUTER_PATH_SEP);
	char buf[1024];
	snprintf(buf, sizeof(buf), "%s", str.c_str());
	char * base = basename(buf);
	if (!base || strlen(base) == 0) {
		char lbuf[128];
		time_t t = time(NULL);
		strftime(lbuf, sizeof(lbuf), "%Y-%b-%d-%H%M%S.unknown", localtime(&t));
		fn.append(lbuf);
	} else {
		fn.append(utils::replace_all(base, "'", "%27"));
	}
	return fn;
}

Project: newsbeuter
File: queueloader.cpp

void queueloader::reload(std::vector<download>& downloads, bool remove_unplayed) {
	std::vector<download> dltemp;
	std::fstream f;

	for (auto dl : downloads) {
		if (dl.status() == DL_DOWNLOADING) { // we are not allowed to reload if a download is in progress!
			LOG(LOG_INFO, "queueloader::reload: aborting reload due to DL_DOWNLOADING status");
			return;
		}
		switch (dl.status()) {
		case DL_QUEUED:
		case DL_CANCELLED:
		case DL_FAILED:
		case DL_ALREADY_DOWNLOADED:
		case DL_READY:
			LOG(LOG_DEBUG, "queueloader::reload: storing %s to new vector", dl.url());
			dltemp.push_back(dl);
			break;
		case DL_PLAYED:
		case DL_FINISHED:
			if (!remove_unplayed) {
				LOG(LOG_DEBUG, "queueloader::reload: storing %s to new vector", dl.url());
				dltemp.push_back(dl);
			}
			break;
		default:
			break;
		}
	}

	f.open(queuefile.c_str(), std::fstream::in);
	if (f.is_open()) {
		std::string line;
		do {
			getline(f, line);
			if (!f.eof() && line.length() > 0) {
				LOG(LOG_DEBUG, "queueloader::reload: loaded `%s' from queue file", line.c_str());
				std::vector<std::string> fields = utils::tokenize_quoted(line);
				bool url_found = false;

				for (auto dl : dltemp) {
					if (fields[0] == dl.url()) {
						LOG(LOG_INFO, "queueloader::reload: found `%s' in old vector", fields[0].c_str());
						url_found = true;
						break;
					}
				}

				for (auto dl : downloads) {
					if (fields[0] == dl.url()) {
						LOG(LOG_INFO, "queueloader::reload: found `%s' in new vector", line.c_str());
						url_found = true;
						break;
					}
				}

				if (!url_found) {
					LOG(LOG_INFO, "queueloader::reload: found `%s' nowhere -> storing to new vector", line.c_str());
					download d(ctrl);
					std::string fn;
					if (fields.size() == 1)
						fn = get_filename(fields[0]);
					else
						fn = fields[1];
					d.set_filename(fn);
					if (access(fn.c_str(), F_OK)==0) {
						LOG(LOG_INFO, "queueloader::reload: found `%s' on file system -> mark as already downloaded", fn.c_str());
						if (fields.size() >= 3) {
							if (fields[2] == "downloaded")
								d.set_status(DL_READY);
							if (fields[2] == "played")
								d.set_status(DL_PLAYED);
						} else
							d.set_status(DL_ALREADY_DOWNLOADED); // TODO: scrap DL_ALREADY_DOWNLOADED state
					}
					d.set_url(fields[0]);
					dltemp.push_back(d);
				}
			}
		} while (!f.eof());
		f.close();
	}

	f.open(queuefile.c_str(), std::fstream::out);
	if (f.is_open()) {
		for (auto dl : dltemp) {
			f << dl.url() << " " << stfl::quote(dl.filename());
			if (dl.status() == DL_READY)
				f << " downloaded";
			if (dl.status() == DL_PLAYED)
				f << " played";
			f << std::endl;
		}
		f.close();
	}

	downloads = dltemp;
}

Project: newsbeuter
File: pb_controller.cpp

bool pb_controller::setup_dirs_xdg(const char *env_home) {
	const char *env_xdg_config;
	const char *env_xdg_data;
	std::string xdg_config_dir;
	std::string xdg_data_dir;

	env_xdg_config = ::getenv("XDG_CONFIG_HOME");
	if (env_xdg_config) {
		xdg_config_dir = env_xdg_config;
	} else {
		xdg_config_dir = env_home;
		xdg_config_dir.append(NEWSBEUTER_PATH_SEP);
		xdg_config_dir.append(".config");
	}

	env_xdg_data = ::getenv("XDG_DATA_HOME");
	if (env_xdg_data) {
		xdg_data_dir = env_xdg_data;
	} else {
		xdg_data_dir = env_home;
		xdg_data_dir.append(NEWSBEUTER_PATH_SEP);
		xdg_data_dir.append(".local");
		xdg_data_dir.append(NEWSBEUTER_PATH_SEP);
		xdg_data_dir.append("share");
	}

	xdg_config_dir.append(NEWSBEUTER_PATH_SEP);
	xdg_config_dir.append(NEWSBEUTER_SUBDIR_XDG);

	xdg_data_dir.append(NEWSBEUTER_PATH_SEP);
	xdg_data_dir.append(NEWSBEUTER_SUBDIR_XDG);

	if (access(xdg_config_dir.c_str(), R_OK | X_OK) != 0) {
		std::cout << utils::strprintf(_("XDG: configuration directory '%s' not accessible, using '%s' instead."), xdg_config_dir.c_str(), config_dir.c_str()) << std::endl;
		return false;
	}
	if (access(xdg_data_dir.c_str(), R_OK | X_OK | W_OK) != 0) {
		std::cout << utils::strprintf(_("XDG: data directory '%s' not accessible, using '%s' instead."), xdg_data_dir.c_str(), config_dir.c_str()) << std::endl;
		return false;
	}

	config_dir = xdg_config_dir;

	/* in config */
	url_file = config_dir + std::string(NEWSBEUTER_PATH_SEP) + url_file;
	config_file = config_dir + std::string(NEWSBEUTER_PATH_SEP) + config_file;

	/* in data */
	cache_file = xdg_data_dir + std::string(NEWSBEUTER_PATH_SEP) + cache_file;
	lock_file = cache_file + LOCK_SUFFIX;
	queue_file = xdg_data_dir + std::string(NEWSBEUTER_PATH_SEP) + queue_file;
	searchfile = utils::strprintf("%s%shistory.search", xdg_data_dir.c_str(), NEWSBEUTER_PATH_SEP);
	cmdlinefile = utils::strprintf("%s%shistory.cmdline", xdg_data_dir.c_str(), NEWSBEUTER_PATH_SEP);

	return true;
}

Project: newsbeuter
File: pb_controller.cpp

pb_controller::pb_controller() : v(0), config_file("config"), queue_file("queue"), cfg(0), view_update_(true),  max_dls(1), ql(0) {
	char * cfgdir;
	if (!(cfgdir = ::getenv("HOME"))) {
		struct passwd * spw = ::getpwuid(::getuid());
		if (spw) {
			cfgdir = spw->pw_dir;
		} else {
			std::cout << _("Fatal error: couldn't determine home directory!") << std::endl;
			std::cout << utils::strprintf(_("Please set the HOME environment variable or add a valid user for UID %u!"), ::getuid()) << std::endl;
			::exit(EXIT_FAILURE);
		}
	}
	config_dir = cfgdir;

	if (setup_dirs_xdg(cfgdir))
		return;

	config_dir.append(NEWSBEUTER_PATH_SEP);
	config_dir.append(NEWSBEUTER_CONFIG_SUBDIR);
	::mkdir(config_dir.c_str(),0700); // create configuration directory if it doesn't exist

	config_file = config_dir + std::string(NEWSBEUTER_PATH_SEP) + config_file;
	queue_file = config_dir + std::string(NEWSBEUTER_PATH_SEP) + queue_file;
	lock_file = config_dir + std::string(NEWSBEUTER_PATH_SEP) + lock_file;
}

Project: newsbeuter
File: pb_controller.cpp

void pb_controller::play_file(const std::string& file) {
	std::string cmdline;
	std::string player = cfg->get_configvalue("player");
	if (player == "")
		return;
	cmdline.append(player);
	cmdline.append(" \'");
	cmdline.append(utils::replace_all(file,"'", "%27"));
	cmdline.append("\'");
	stfl::reset();
	LOG(LOG_DEBUG, "pb_controller::play_file: running `%s'", cmdline.c_str());
	::system(cmdline.c_str());
}