Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion args.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -2949,7 +2949,20 @@ namespace args
if (optionType == OptionType::LongFlag && allowJoinedLongValue)
{
const auto separator = longseparator.empty() ? chunk.npos : chunk.find(longseparator);
if (separator != chunk.npos)
// Only attempt joined-value completion when the
// separator lies at or past the long prefix, so
// there is a (possibly empty) flag name between
// them. With a custom longseparator that overlaps
// the prefix (e.g. LongSeparator("-") under the
// default "--" prefix), an attacker-controlled
// completion word like "--x" puts the separator
// inside the prefix, making `arg` shorter than
// longprefix. arg.substr(longprefix.size()) would
// then throw std::out_of_range, which escapes the
// parser as a non-args exception (bypassing the
// documented catch(args::Error) idiom) and is
// thrown even under ARGS_NOEXCEPT.
if (separator != chunk.npos && separator >= longprefix.size())
{
std::string arg(chunk, 0, separator);
if (auto flag = this->Match(arg.substr(longprefix.size())))
Expand Down
49 changes: 49 additions & 0 deletions test/completion_separator_prefix_overlap.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* Copyright (c) Taylor Richberger <taylor@axfive.net>
* This code is released under the license described in the LICENSE file
*
* Regression test for an out-of-bounds std::string::substr in the bash
* completion handler.
*
* When a custom LongSeparator overlaps the long prefix (here "-" under the
* default "--" prefix), an attacker-controlled completion word such as
* "--x" makes chunk.find(longseparator) land *inside* the prefix. The
* handler then built `arg` (= chunk up to the separator) shorter than
* longprefix and called arg.substr(longprefix.size()), which threw
* std::out_of_range.
*
* That std::out_of_range is not an args::Error, so it escaped past the
* documented `catch (args::Completion&) / catch (args::Error&)` idiom and
* terminated the program. After the fix the joined-value completion is
* simply skipped when the separator falls inside the prefix, so the normal
* args::Completion path runs instead.
*/

#include "test_common.hxx"

#include <args.hxx>

#include "test_helpers.hxx"

int main()
{
args::ArgumentParser p("parser");
args::CompletionFlag c(p, {"completion"});
args::ValueFlag<std::string> f(p, "f", "f", {'f', "foo"}, "abc");
p.LongSeparator("-"); // legal, non-empty separator that overlaps "--"

// Must throw args::Completion (the normal completion control-flow), not
// a leaked std::out_of_range.
test::require_throws_as<args::Completion>([&]
{
p.ParseArgs(std::vector<std::string>{"--completion", "bash", "1", "test", "--x"});
});

// A separator exactly at the prefix boundary leaves an empty flag name;
// it must also complete cleanly rather than throw out_of_range.
test::require_throws_as<args::Completion>([&]
{
p.ParseArgs(std::vector<std::string>{"--completion", "bash", "1", "test", "---x"});
});

return 0;
}
43 changes: 43 additions & 0 deletions test/noexcept_completion_separator_prefix_overlap.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* Copyright (c) Taylor Richberger <taylor@axfive.net>
* This code is released under the license described in the LICENSE file
*
* ARGS_NOEXCEPT counterpart of completion_separator_prefix_overlap.
*
* std::string::substr throws std::out_of_range regardless of ARGS_NOEXCEPT,
* so the unguarded arg.substr(longprefix.size()) in the bash completion
* handler broke the no-throw contract: an ARGS_NOEXCEPT build (which may be
* compiled with -fno-exceptions) would std::terminate on the completion
* word "--x" when a custom LongSeparator overlaps the long prefix.
*
* After the fix ParseArgs returns normally and records the completion via
* the error state instead of throwing.
*/

#define ARGS_NOEXCEPT
#include "test_common.hxx"

#include <args.hxx>

#include "test_helpers.hxx"

int main()
{
args::ArgumentParser p("parser");
args::CompletionFlag c(p, {"completion"});
args::ValueFlag<std::string> f(p, "f", "f", {'f', "foo"}, "abc");
p.LongSeparator("-");

test::require_nothrow([&]
{
p.ParseArgs(std::vector<std::string>{"--completion", "bash", "1", "test", "--x"});
});
test::require(p.GetError() == args::Error::Completion);

test::require_nothrow([&]
{
p.ParseArgs(std::vector<std::string>{"--completion", "bash", "1", "test", "---x"});
});
test::require(p.GetError() == args::Error::Completion);

return 0;
}
Loading