reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

commit 4423e81d1efb2f4794f745ca853c548650c299b0
parent 5ec230addfc7ba95aa8cbb863df704e8e93240ec
Author: cfillion <cfillion@users.noreply.github.com>
Date:   Fri, 12 Feb 2016 00:40:19 -0500

harden against directory traversal vulnerability

Diffstat:
Msrc/package.cpp | 6++++--
Msrc/package.hpp | 2+-
Msrc/path.cpp | 60+++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/path.hpp | 6+++++-
Msrc/remote.cpp | 10++++++----
Msrc/source.cpp | 2+-
Mtest/package.cpp | 21+++++++++++++++++----
Mtest/path.cpp | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/remote.cpp | 20+++++++++++++++++++-
Mtest/source.cpp | 17+++++++++++++++++
10 files changed, 170 insertions(+), 25 deletions(-)

diff --git a/src/package.cpp b/src/package.cpp @@ -79,7 +79,7 @@ const Version *Package::lastVersion() const return *m_versions.rbegin(); } -Path Package::targetPath() const +Path Package::makeTargetPath(const string &file) const { Path path; @@ -90,7 +90,9 @@ Path Package::targetPath() const case ScriptType: path.append("Scripts"); path.append(m_category->index()->name()); - path.append(m_category->name()); + + // only allow directory traversal up to the index name + path += Path(m_category->name()) + file; break; default: throw reapack_error("unsupported package type"); diff --git a/src/package.hpp b/src/package.hpp @@ -48,7 +48,7 @@ public: const Version *version(size_t index) const; const Version *lastVersion() const; - Path targetPath() const; + Path makeTargetPath(const std::string &file = {}) const; private: Category *m_category; diff --git a/src/path.cpp b/src/path.cpp @@ -18,6 +18,7 @@ #include "path.hpp" #include <algorithm> +#include <boost/range/adaptor/reversed.hpp> #include <vector> using namespace std; @@ -28,6 +29,9 @@ static const char SEPARATOR = '/'; static const char SEPARATOR = '\\'; #endif +static const string DOT = "."; +static const string DOTDOT = ".."; + Path Path::s_root; static vector<string> Split(const string &input) @@ -46,7 +50,7 @@ static vector<string> Split(const string &input) const string part = input.substr(last, pos - last); - if(!part.empty()) + if(!part.empty() && part != ".") list.push_back(part); last = pos + 1; @@ -60,22 +64,48 @@ Path::Path(const std::string &path) append(path); } -void Path::prepend(const string &parts) +void Path::prepend(const string &str) { - if(parts.empty()) + if(str.empty()) return; - for(const string &part : Split(parts)) - m_parts.push_front(part); + bool skip = false; + + const vector<string> &parts = Split(str); + + for(const string &part : parts | boost::adaptors::reversed) { + if(part == DOTDOT) + skip = true; + else if(!skip) + m_parts.push_front(part); + else + skip = false; + } } -void Path::append(const string &parts) +void Path::append(const string &parts, const bool traversal) { if(parts.empty()) return; - for(const string &part : Split(parts)) - m_parts.push_back(part); + for(const string &part : Split(parts)) { + if(part == DOTDOT) { + if(traversal) + removeLast(); + } + else + m_parts.push_back(part); + } +} + +void Path::prepend(const Path &o) +{ + m_parts.insert(m_parts.begin(), o.m_parts.begin(), o.m_parts.end()); +} + +void Path::append(const Path &o) +{ + m_parts.insert(m_parts.end(), o.m_parts.begin(), o.m_parts.end()); } void Path::clear() @@ -166,11 +196,23 @@ Path Path::operator+(const string &part) const Path Path::operator+(const Path &o) const { Path path(*this); - path.m_parts.insert(path.m_parts.end(), o.m_parts.begin(), o.m_parts.end()); + path += o; return path; } +const Path &Path::operator+=(const string &parts) +{ + append(parts); + return *this; +} + +const Path &Path::operator+=(const Path &o) +{ + append(o); + return *this; +} + const string &Path::at(const size_t index) const { auto it = m_parts.begin(); diff --git a/src/path.hpp b/src/path.hpp @@ -33,7 +33,9 @@ public: Path(const std::string &path = std::string()); void prepend(const std::string &part); - void append(const std::string &part); + void append(const std::string &part, bool traversal = true); + void prepend(const Path &other); + void append(const Path &other); void removeLast(); void clear(); @@ -51,6 +53,8 @@ public: bool operator<(const Path &) const; Path operator+(const std::string &) const; Path operator+(const Path &) const; + const Path &operator+=(const std::string &); + const Path &operator+=(const Path &); std::string &operator[](size_t); const std::string &operator[](size_t) const; diff --git a/src/remote.cpp b/src/remote.cpp @@ -30,12 +30,14 @@ static char DATA_DELIMITER = '|'; static bool ValidateName(const string &name) { - static const regex pattern("^[^~#%&*{}\\\\:<>?/+|\"]+$"); + static const regex validPattern("[^~#%&*{}\\\\:<>?/+|\"]+"); + static const regex invalidPattern("\\.+"); - smatch match; - regex_match(name, match, pattern); + smatch match, invalid; + regex_match(name, match, validPattern); + regex_match(name, invalid, invalidPattern); - return !match.empty(); + return !match.empty() && invalid.empty(); } static bool ValidateUrl(const string &url) diff --git a/src/source.cpp b/src/source.cpp @@ -86,5 +86,5 @@ Path Source::targetPath() const if(!pkg) throw reapack_error("no package associated with the source"); - return pkg->targetPath() + file(); + return pkg->makeTargetPath(file()); } diff --git a/test/package.cpp b/test/package.cpp @@ -81,14 +81,14 @@ TEST_CASE("add owned version", M) { } } -TEST_CASE("unknown target path", M) { +TEST_CASE("target path for unknown package type", M) { RemoteIndex ri("name"); Category cat("name", &ri); Package pack(Package::UnknownType, "a", &cat); try { - pack.targetPath(); + pack.makeTargetPath({}); FAIL(); } catch(const reapack_error &e) { @@ -107,14 +107,27 @@ TEST_CASE("script target path", M) { expected.append("Remote Name"); expected.append("Category Name"); - REQUIRE(pack.targetPath() == expected); + REQUIRE(pack.makeTargetPath() == expected); +} + +TEST_CASE("script target path: directory traversal", M) { + RemoteIndex ri("Remote Name"); + Category cat("../..", &ri); + + Package pack(Package::ScriptType, "file.name", &cat); + + Path expected; + expected.append("Scripts"); + expected.append("Remote Name"); + + REQUIRE(pack.makeTargetPath() == expected); } TEST_CASE("script target path without category", M) { Package pack(Package::ScriptType, "file.name"); try { - pack.targetPath(); + pack.makeTargetPath(); FAIL(); } catch(const reapack_error &e) { diff --git a/test/path.cpp b/test/path.cpp @@ -62,7 +62,7 @@ TEST_CASE("concatenate paths", M) { Path c; c.append("hello"); - c.append("world"); + c += "world"; REQUIRE(a + b == c); REQUIRE(a + "world" == c); @@ -123,13 +123,17 @@ TEST_CASE("split input", M) { a.prepend("hello/world"); REQUIRE(a.size() == 2); + REQUIRE(a[0] == "hello"); + REQUIRE(a[1] == "world"); } SECTION("append") { Path a; a.append("hello/world"); - REQUIRE(a.size() == 2); + + a += "chunky/bacon"; + REQUIRE(a.size() == 4); } SECTION("skip empty parts") { @@ -193,3 +197,46 @@ TEST_CASE("first and last path component", M) { REQUIRE(path.first() == "hello"); REQUIRE(path.last() == "bacon"); } + +TEST_CASE("directory traversal", M) { + SECTION("append (enabled)") { + REQUIRE(Path("a/./b") == Path("a/b")); + REQUIRE(Path("a/../b") == Path("b")); + REQUIRE(Path("../a") == Path("a")); + } + + SECTION("append (disabled)") { + Path a; + a.append("a/../b", false); + REQUIRE(a == Path("a/b")); + } + + SECTION("prepend") { + Path a; + a.prepend("a/../b/c/../d"); + REQUIRE(a == Path("b/d")); + + Path b; + b.prepend("../a"); + REQUIRE(b == Path("a")); + } + + SECTION("concatenate") { + // concatenating a std::string to a path, directory traversal is applied + const Path a = Path("a/b/c") + "../../../d/e/f"; + REQUIRE(a == Path("d/e/f")); + + // here, the directory traversal components are lost before concatenating + const Path b = Path("a/b/c") + Path("../../../d/e/f"); + REQUIRE(b == Path("a/b/c/d/e/f")); + } +} + +TEST_CASE("append and prepend full paths") { + Path a; + a += Path("c/d"); + a.append(Path("e/f")); + a.prepend(Path("a/b")); + + REQUIRE(a == Path("a/b/c/d/e/f")); +} diff --git a/test/remote.cpp b/test/remote.cpp @@ -31,7 +31,7 @@ TEST_CASE("construct invalid remote", M) { SECTION("invalid name") { try { - Remote remote("/", "url"); + Remote remote("a/", "url"); FAIL(); } catch(const reapack_error &e) { @@ -39,6 +39,24 @@ TEST_CASE("construct invalid remote", M) { } } + SECTION("directory traversal in name") { + try { + Remote remote("..", "url"); + FAIL("dotdot was allowed"); + } + catch(const reapack_error &e) { + REQUIRE(string(e.what()) == "invalid name"); + } + + try { + Remote remote(".", "url"); + FAIL("single dot was allowed"); + } + catch(const reapack_error &e) { + REQUIRE(string(e.what()) == "invalid name"); + } + } + SECTION("empty url") { try { Remote remote("name", {}); diff --git a/test/source.cpp b/test/source.cpp @@ -129,6 +129,23 @@ TEST_CASE("source target path", M) { REQUIRE(source.targetPath() == expected); } +TEST_CASE("source target path with parent directory traversal", M) { + RemoteIndex ri("RemoteIndex Name"); + Category cat("Category Name", &ri); + Package pack(Package::ScriptType, "package name", &cat); + Version ver("1.0", &pack); + + const Source source(Source::GenericPlatform, "../../../file.name", "url", &ver); + + Path expected; + expected.append("Scripts"); + expected.append("RemoteIndex Name"); + // expected.append("Category Name"); // only the category can be bypassed! + expected.append("file.name"); + + REQUIRE(source.targetPath() == expected); +} + TEST_CASE("source target path without package", M) { try { const Source source(Source::GenericPlatform, "a", "b");