merged RC_1_2 into RC_2_0

This commit is contained in:
arvidn
2020-12-03 00:20:04 +01:00
26 changed files with 618 additions and 51 deletions

View File

@ -106,7 +106,7 @@ jobs:
- name: install boost
run: |
sudo apt install libboost-tools-dev libboost-dev
echo "using clang_tidy : : clang-tidy \"-checks=-clang-analyzer-core.*,-clang-analyzer-unix.*\" : <cxxflags>-std=c++11 <cxxflags>-I/usr/local/clang-7.0.0/include/c++/v1 <cxxflags>-stdlib=libc++ <linkflags>-stdlib=libc++ ;" >> ~/user-config.jam;
echo "using clang_tidy : : clang-tidy \"-checks=-clang-analyzer-core.*,-clang-analyzer-unix.*\" : <cxxflags>-std=c++14 <cxxflags>-I/usr/local/clang-7.0.0/include/c++/v1 <cxxflags>-stdlib=libc++ <linkflags>-stdlib=libc++ ;" >> ~/user-config.jam;
- name: analyze
run: |
@ -196,3 +196,34 @@ jobs:
run: |
cd bindings/python
LD_LIBRARY_PATH=./dependencies python3 test.py
dist:
name: build dist
runs-on: ubuntu-20.04
steps:
- name: checkout
uses: actions/checkout@v2.3.3
with:
submodules: recursive
- name: install boost
run: |
sudo apt install libboost-tools-dev libboost-python-dev libboost-dev libboost-system-dev
sudo apt install python-docutils python-pygments python-pil gsfonts inkscape icoutils graphviz hunspell imagemagick
python -m pip install aafigure
echo "using gcc ;" >>~/user-config.jam
- name: build tarball
run: AAFIGURE=~/.local/bin/aafigure RST2HTML=rst2html make dist
- uses: actions/upload-artifact@v2
with:
name: tarball
path: libtorrent-rasterbar-*.tar.gz
- name: test-tarball
run: |
tar xvzf libtorrent-rasterbar-*.tar.gz
cd libtorrent-rasterbar-*/test
b2 link=static -j2 cxxstd=14 testing.execute=off

View File

@ -43,8 +43,9 @@
* libtorrent now requires C++14 to build
* added support for GnuTLS for HTTPS and torrents over SSL
* validate HTTPS certificates by default (trackers and web seeds)
* load SSL certificates from windows system certificate store, to authenticate trackers
* introduce mitigation for Server Side Request Forgery in tracker announces
* introduce mitigation for Server Side Request Forgery in tracker and web seed URLs
* fix error handling for pool allocation failure
1.2.11 released

View File

@ -622,6 +622,7 @@ HEADERS = \
aux_/packet_pool.hpp \
aux_/path.hpp \
aux_/polymorphic_socket.hpp \
aux_/pool.hpp \
aux_/portmap.hpp \
aux_/posix_part_file.hpp \
aux_/posix_storage.hpp \

View File

@ -72,6 +72,8 @@ fuzzer dht_node ;
fuzzer utp ;
fuzzer resume_data ;
fuzzer peer_conn ;
fuzzer idna ;
fuzzer parse_url ;
fuzzer session_params ;
fuzzer add_torrent ;

42
fuzzers/src/idna.cpp Normal file
View File

@ -0,0 +1,42 @@
/*
Copyright (c) 2020, Arvid Norberg
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include "libtorrent/parse_url.hpp"
#include "libtorrent/string_view.hpp"
#include <cstdint>
extern "C" int LLVMFuzzerTestOneInput(std::uint8_t const* data, size_t size)
{
lt::is_idna(lt::string_view(reinterpret_cast<char const*>(data), size));
return 0;
}

43
fuzzers/src/parse_url.cpp Normal file
View File

@ -0,0 +1,43 @@
/*
Copyright (c) 2020, Arvid Norberg
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include "libtorrent/parse_url.hpp"
#include <cstdint>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(std::uint8_t const* data, size_t size)
{
lt::error_code ec;
lt::parse_url_components(std::string(reinterpret_cast<char const*>(data), size), ec);
return 0;
}

View File

@ -10,7 +10,7 @@ corpus_dirs = [
'dht_node', 'escape_path', 'escape_string', 'file_storage_add_file',
'http_parser', 'lazy_bdecode', 'parse_int', 'parse_magnet_uri', 'resume_data',
'sanitize_path', 'utf8_codepoint', 'utp',
'verify_encoding', 'peer_conn', 'add_torrent']
'verify_encoding', 'peer_conn', 'add_torrent', 'idna', 'parse_url']
for p in corpus_dirs:
try:
@ -31,7 +31,7 @@ xml_tests = [
'<selfclosing />']
for x in xml_tests:
name = hashlib.sha1(x).hexdigest()
name = hashlib.sha1(x.encode('ascii')).hexdigest()
with open(os.path.join('corpus', 'upnp', name), 'w+') as f:
f.write(x)
@ -39,6 +39,20 @@ gzip_dir = '../test'
for f in ['zeroes.gz', 'corrupt.gz', 'invalid1.gz']:
shutil.copy(os.path.join(gzip_dir, f), os.path.join('corpus', 'gzip'))
idna = ['....', 'xn--foo-.bar', 'foo.xn--bar-.com', 'Xn--foobar-', 'XN--foobar-', '..xnxn--foobar-']
counter = 0
for i in idna:
open(os.path.join('corpus', 'idna', '%d' % counter), 'w+').write(i)
counter += 1
urls = ['https://user:password@example.com:8080/path?query']
counter = 0
for i in urls:
open(os.path.join('corpus', 'parse_url', '%d' % counter), 'w+').write(i)
counter += 1
# generate peer protocol messages
messages = []
@ -48,13 +62,13 @@ def add_length(msg):
def add_reserved(msg):
return '\0\0\0\0\0\x18\0\x05' + msg
return b'\0\0\0\0\0\x18\0\x05' + msg
# extended handshake
def add_extended_handshake(msg):
ext_handshake = 'd1:md11:ut_metadatai1e11:lt_donthavei2e12:ut_holepunch' + \
'i3e11:upload_onlyi4ee11:upload_onlyi1e10:share_modei1e4:reqqi1234e6:yourip4:0000e'
ext_handshake = b'd1:md11:ut_metadatai1e11:lt_donthavei2e12:ut_holepunch' + \
b'i3e11:upload_onlyi4ee11:upload_onlyi1e10:share_modei1e4:reqqi1234e6:yourip4:0000e'
return add_length(struct.pack('BB', 20, 0) + ext_handshake) + msg
@ -70,7 +84,7 @@ for i in range(101):
# piece
for i in range(101):
messages.append(add_length(struct.pack('>Bii', 7, i, 0) + ('a' * 0x4000)))
messages.append(add_length(struct.pack('>Bii', 7, i, 0) + (b'a' * 0x4000)))
# single-byte
for i in range(256):
@ -115,7 +129,7 @@ for i in range(-10, 200, 20):
# hash
for i in range(-10, 200, 20):
for j in range(-1, 1):
messages.append(add_length(struct.pack('>Biiiii', 22, i, j, 0, 2, 0) + ('0' * 32 * 5)))
messages.append(add_length(struct.pack('>Biiiii', 22, i, j, 0, 2, 0) + (b'0' * 32 * 5)))
# lt_dont_have
messages.append(add_extended_handshake(add_length(struct.pack('>BBi', 20, 7, -1))))
@ -141,17 +155,17 @@ for i in range(0, 1):
bitfield_len = (100 + 7) // 8
for i in range(256):
messages.append(add_length(struct.pack('B', 5) + (chr(i) * bitfield_len)))
messages.append(add_length(struct.pack('B', 5) + (struct.pack('B', i) * bitfield_len)))
mixes = []
for i in range(200):
shuffle(messages)
mixes.append(''.join(messages[1:20]))
mixes.append(b''.join(messages[1:20]))
messages += mixes
for m in messages:
f = open('corpus/peer_conn/%s' % hashlib.sha1(m).hexdigest(), 'w+')
f = open('corpus/peer_conn/%s' % hashlib.sha1(m).hexdigest(), 'wb+')
f.write(add_reserved(m))
f.close()

View File

@ -1763,7 +1763,8 @@ TORRENT_VERSION_NAMESPACE_3
privileged_ports,
utp_disabled,
tcp_disabled,
invalid_local_interface
invalid_local_interface,
ssrf_mitigation
};
// the reason for the peer being blocked. Is one of the values from the

View File

@ -69,6 +69,7 @@ using http_handler = std::function<void(error_code const&
using http_connect_handler = std::function<void(http_connection&)>;
using http_filter_handler = std::function<void(http_connection&, std::vector<tcp::endpoint>&)>;
using hostname_filter_handler = std::function<bool(http_connection&, string_view)>;
// when bottled, the last two arguments to the handler
// will always be 0
@ -84,6 +85,7 @@ struct TORRENT_EXTRA_EXPORT http_connection
, int max_bottled_buffer_size
, http_connect_handler ch
, http_filter_handler fh
, hostname_filter_handler hfh
#if TORRENT_USE_SSL
, ssl::context* ssl_ctx
#endif
@ -177,6 +179,7 @@ private:
http_handler m_handler;
http_connect_handler m_connect_handler;
http_filter_handler m_filter_handler;
hostname_filter_handler m_hostname_filter_handler;
deadline_timer m_timer;
time_duration m_completion_timeout;

View File

@ -74,6 +74,7 @@ namespace libtorrent {
}
void on_filter(http_connection& c, std::vector<tcp::endpoint>& endpoints);
bool on_filter_hostname(http_connection& c, string_view hostname);
void on_connect(http_connection& c);
void on_response(error_code const& ec, http_parser const& parser
, span<char const> data);

View File

@ -39,6 +39,7 @@ POSSIBILITY OF SUCH DAMAGE.
#include <string>
#include "libtorrent/error_code.hpp"
#include "libtorrent/string_view.hpp"
namespace libtorrent {
@ -50,6 +51,10 @@ namespace libtorrent {
// split a URL in its base and path parts
TORRENT_EXTRA_EXPORT std::tuple<std::string, std::string>
split_url(std::string url, error_code& ec);
// returns true if the hostname contains any IDNA (internationalized domain
// name) labels.
TORRENT_EXTRA_EXPORT bool is_idna(string_view hostname);
}
#endif

View File

@ -938,17 +938,33 @@ namespace aux {
// small piece sizes
piece_extent_affinity,
// when set to true, the certificate of HTTPS trackers will be
// validated against the system's certificate store (as defined by
// OpenSSL). If the system does not have one, enabling this may cause
// HTTPS trackers to fail.
// when set to true, the certificate of HTTPS trackers and HTTPS web
// seeds will be validated against the system's certificate store
// (as defined by OpenSSL). If the system does not have a
// certificate store, this option may have to be disabled in order
// to get trackers and web seeds to work).
validate_https_trackers,
// when enabled, any HTTP(S) tracker requests to localhost (loopback)
// when enabled, tracker and web seed requests are subject to
// certain restrictions.
//
// An HTTP(s) tracker requests to localhost (loopback)
// must have the request path start with "/announce". This is the
// conventional bittorrent tracker request. Any other HTTP(S)
// tracker request to loopback will be ignored.
tracker_ssrf_mitigation,
// tracker request to loopback will be rejected. This applies to
// trackers that redirect to loopback as well.
//
// Web seeds that end up on the client's local network (i.e. in a
// private IP address range) may not include query string arguments.
// This applies to web seeds redirecting to the local network as
// well.
ssrf_mitigation,
// when disabled, any tracker or web seed with an IDNA hostname
// (internationalized domain name) is ignored. This is a security
// precaution to avoid various unicode encoding attacks that might
// happen at the application level.
allow_idna,
// when set to true, enables the attempt to use SetFileValidData()
// to pre-allocate disk space. This system call will only work when

View File

@ -182,6 +182,7 @@ std::shared_ptr<http_connection> test_request(io_context& ios
std::printf("CONNECTED: %s\n", url.c_str());
}
, lt::http_filter_handler()
, lt::hostname_filter_handler()
#if TORRENT_USE_SSL
, &ssl_ctx
#endif
@ -641,6 +642,7 @@ TORRENT_TEST(http_connection_ssl_proxy)
}
, true, 1024*1024, lt::http_connect_handler()
, http_filter_handler()
, hostname_filter_handler()
#if TORRENT_USE_SSL
, &ssl_ctx
#endif

View File

@ -326,6 +326,16 @@ struct sim_config : sim::default_config
result.push_back(make_address_v6("::1"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(1));
}
if (hostname == "xn--tracker-.com")
{
result.push_back(make_address_v4("123.0.0.2"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
}
if (hostname == "redirector.com")
{
result.push_back(make_address_v4("123.0.0.4"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
}
return default_config::hostname_lookup(requestor, hostname, result, ec);
}
@ -622,7 +632,8 @@ TORRENT_TEST(ipv6_support_bind_v6_v4)
// port 8080.
template <typename Setup, typename Announce, typename Test1, typename Test2>
void tracker_test(Setup setup, Announce a, Test1 test1, Test2 test2
, char const* url_path = "/announce")
, char const* url_path = "/announce"
, char const* redirect = "http://123.0.0.2/announce")
{
using sim::asio::ip::address_v4;
sim_config network_cfg;
@ -630,6 +641,7 @@ void tracker_test(Setup setup, Announce a, Test1 test1, Test2 test2
sim::asio::io_context tracker_ios(sim, make_address_v4("123.0.0.2"));
sim::asio::io_context tracker_ios6(sim, make_address_v6("ff::dead:beef"));
sim::asio::io_context redirector_ios(sim, make_address_v4("123.0.0.4"));
sim::asio::io_context tracker_lo_ios(sim, make_address_v4("127.0.0.1"));
sim::asio::io_context tracker_lo_ios6(sim, make_address_v6("::1"));
@ -639,11 +651,13 @@ void tracker_test(Setup setup, Announce a, Test1 test1, Test2 test2
sim::http_server http6(tracker_ios6, 8080);
sim::http_server http_lo(tracker_lo_ios, 8080);
sim::http_server http6_lo(tracker_lo_ios6, 8080);
sim::http_server http_redirect(redirector_ios, 8080);
http.register_handler(url_path, a);
http6.register_handler(url_path, a);
http_lo.register_handler(url_path, a);
http6_lo.register_handler(url_path, a);
http_redirect.register_redirect(url_path, redirect);
lt::session_proxy zombie;
@ -1345,15 +1359,15 @@ TORRENT_TEST(tracker_user_agent_privacy_mode_private_torrent)
TEST_EQUAL(got_announce, true);
}
void test_ssrf(char const* announce_path, bool const feature_on
, char const* tracker_url, bool const expect_announce)
bool test_ssrf(char const* announce_path, bool const feature_on
, char const* tracker_url)
{
bool got_announce = false;
tracker_test(
[&](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::tracker_ssrf_mitigation, feature_on);
pack.set_bool(settings_pack::ssrf_mitigation, feature_on);
ses.apply_settings(pack);
p.trackers.emplace_back(tracker_url);
return 60;
@ -1367,28 +1381,73 @@ void test_ssrf(char const* announce_path, bool const feature_on
, [](torrent_handle h) {}
, [](torrent_handle h) {}
, announce_path);
TEST_EQUAL(got_announce, expect_announce);
return got_announce;
}
TORRENT_TEST(tracker_ssrf_localhost)
TORRENT_TEST(ssrf_localhost)
{
test_ssrf("/announce", true, "http://localhost:8080/announce", true);
test_ssrf("/unusual-announce-path", true, "http://localhost:8080/unusual-announce-path", false);
test_ssrf("/unusual-announce-path", false, "http://localhost:8080/unusual-announce-path", true);
TEST_CHECK(test_ssrf("/announce", true, "http://localhost:8080/announce"));
TEST_CHECK(!test_ssrf("/unusual-announce-path", true, "http://localhost:8080/unusual-announce-path"));
TEST_CHECK(test_ssrf("/unusual-announce-path", false, "http://localhost:8080/unusual-announce-path"));
TEST_CHECK(!test_ssrf("/short", true, "http://localhost:8080/short"));
TEST_CHECK(test_ssrf("/short", false, "http://localhost:8080/short"));
}
TORRENT_TEST(tracker_ssrf_IPv4)
TORRENT_TEST(ssrf_IPv4)
{
test_ssrf("/announce", true, "http://127.0.0.1:8080/announce", true);
test_ssrf("/unusual-announce-path", true, "http://127.0.0.1:8080/unusual-announce-path", false);
test_ssrf("/unusual-announce-path", false, "http://127.0.0.1:8080/unusual-announce-path", true);
TEST_CHECK(test_ssrf("/announce", true, "http://127.0.0.1:8080/announce"));
TEST_CHECK(!test_ssrf("/unusual-announce-path", true, "http://127.0.0.1:8080/unusual-announce-path"));
TEST_CHECK(test_ssrf("/unusual-announce-path", false, "http://127.0.0.1:8080/unusual-announce-path"));
}
TORRENT_TEST(tracker_ssrf_IPv6)
TORRENT_TEST(ssrf_IPv6)
{
test_ssrf("/announce", true, "http://[::1]:8080/announce", true);
test_ssrf("/unusual-announce-path", true, "http://[::1]:8080/unusual-announce-path", false);
test_ssrf("/unusual-announce-path", false, "http://[::1]:8080/unusual-announce-path", true);
TEST_CHECK(test_ssrf("/announce", true, "http://[::1]:8080/announce"));
TEST_CHECK(!test_ssrf("/unusual-announce-path", true, "http://[::1]:8080/unusual-announce-path"));
TEST_CHECK(test_ssrf("/unusual-announce-path", false, "http://[::1]:8080/unusual-announce-path"));
}
bool test_idna(char const* tracker_url, char const* redirect
, bool const feature_on)
{
bool got_announce = false;
tracker_test(
[&](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::allow_idna, feature_on);
ses.apply_settings(pack);
p.trackers.emplace_back(tracker_url);
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>& headers)
{
got_announce = true;
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle h) {}
, [](torrent_handle h) {}
, "/announce"
, redirect ? redirect : ""
);
return got_announce;
}
TORRENT_TEST(tracker_idna)
{
TEST_EQUAL(test_idna("http://tracker.com:8080/announce", nullptr, true), true);
TEST_EQUAL(test_idna("http://tracker.com:8080/announce", nullptr, false), true);
TEST_EQUAL(test_idna("http://xn--tracker-.com:8080/announce", nullptr, true), true);
TEST_EQUAL(test_idna("http://xn--tracker-.com:8080/announce", nullptr, false), false);
}
TORRENT_TEST(tracker_idna_redirect)
{
TEST_EQUAL(test_idna("http://redirector.com:8080/announce", "http://xn--tracker-.com:8080/announce", true), true);
TEST_EQUAL(test_idna("http://redirector.com:8080/announce", "http://xn--tracker-.com:8080/announce", false), false);
}
// This test sets up two peers, one seed an one downloader. The downloader has

View File

@ -103,6 +103,48 @@ add_torrent_params create_torrent(file_storage& fs, bool const v1_only = false)
ret.save_path = ".";
return ret;
}
struct sim_config : sim::default_config
{
explicit sim_config() {}
chrono::high_resolution_clock::duration hostname_lookup(
asio::ip::address const& requestor
, std::string hostname
, std::vector<asio::ip::address>& result
, boost::system::error_code& ec) override
{
auto const ret = duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
if (hostname == "2.server.com")
{
result.push_back(make_address_v4("2.2.2.2"));
return ret;
}
if (hostname == "2.xn--server-.com")
{
result.push_back(make_address_v4("2.2.2.2"));
return ret;
}
if (hostname == "3.server.com")
{
result.push_back(make_address_v4("3.3.3.3"));
return ret;
}
if (hostname == "3.xn--server-.com")
{
result.push_back(make_address_v4("3.3.3.3"));
return ret;
}
if (hostname == "local-network.com")
{
result.push_back(make_address_v4("192.168.1.13"));
return ret;
}
return default_config::hostname_lookup(requestor, hostname, result, ec);
}
};
// this is the general template for these tests. create the session with custom
// settings (Settings), set up the test, by adding torrents with certain
// arguments (Setup), run the test and verify the end state (Test)
@ -113,7 +155,7 @@ void run_test(Setup const& setup
, lt::seconds const timeout = lt::seconds{100})
{
// setup the simulation
sim::default_config network_cfg;
sim_config network_cfg;
sim::simulation sim{network_cfg};
std::unique_ptr<sim::asio::io_context> ios = make_io_context(sim, 0);
lt::session_proxy zombie;
@ -639,3 +681,151 @@ TORRENT_TEST(web_seed_connection_limit)
TEST_CHECK(std::accumulate(expected.begin(), expected.end(), 0) == 2);
}
bool test_idna(char const* url, char const* redirect, bool allow_idna)
{
using namespace lt;
file_storage fs;
fs.add_file("1", 0xc030);
lt::add_torrent_params params = ::create_torrent(fs);
params.url_seeds.emplace_back(url);
bool seeding = false;
error_code ignore;
remove("1", ignore);
run_test(
[&](lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::allow_idna, allow_idna);
ses.apply_settings(pack);
ses.async_add_torrent(params);
},
[&](lt::session&, lt::alert const* alert) {
if (lt::alert_cast<lt::torrent_finished_alert>(alert))
seeding = true;
},
[&](sim::simulation& sim, lt::session&)
{
// http1 is the root web server that will just redirect requests to
// other servers
sim::asio::io_context web_server1(sim, make_address_v4("2.2.2.2"));
sim::http_server http1(web_server1, 8080);
// redirect file 1 and file 2 to the same servers
if (redirect)
http1.register_redirect("/1", redirect);
// server for serving the content
sim::asio::io_context web_server2(sim, make_address_v4("3.3.3.3"));
sim::http_server http2(web_server2, 8080);
serve_content_for(http2, "/1", fs, file_index_t(0));
sim.run();
}
);
return seeding;
}
TORRENT_TEST(idna)
{
// disallow IDNA hostnames
TEST_EQUAL(test_idna("http://3.server.com:8080", nullptr, false), true);
TEST_EQUAL(test_idna("http://3.xn--server-.com:8080", nullptr, false), false);
// allow IDNA hostnames
TEST_EQUAL(test_idna("http://3.server.com:8080", nullptr, true), true);
TEST_EQUAL(test_idna("http://3.xn--server-.com:8080", nullptr, true), true);
}
TORRENT_TEST(idna_redirect)
{
// disallow IDNA hostnames
TEST_EQUAL(test_idna("http://2.server.com:8080", "http://3.server.com:8080/1", false), true);
TEST_EQUAL(test_idna("http://2.server.com:8080", "http://3.xn--server-.com:8080/1", false), false);
TEST_EQUAL(test_idna("http://2.xn--server-.com:8080", "http://3.server.com:8080/1", false), false);
TEST_EQUAL(test_idna("http://2.xn--server-.com:8080", "http://3.xn--server-.com:8080/1", false), false);
// allow IDNA hostnames
TEST_EQUAL(test_idna("http://2.server.com:8080", "http://3.server.com:8080/1", true), true);
TEST_EQUAL(test_idna("http://2.server.com:8080", "http://3.xn--server-.com:8080/1", true), true);
TEST_EQUAL(test_idna("http://2.xn--server-.com:8080", "http://3.server.com:8080/1", true), true);
TEST_EQUAL(test_idna("http://2.xn--server-.com:8080", "http://3.xn--server-.com:8080/1", true), true);
}
bool test_ssrf(char const* url, char const* redirect, bool enable_feature)
{
using namespace lt;
file_storage fs;
fs.add_file("1", 0xc030);
lt::add_torrent_params params = ::create_torrent(fs);
params.url_seeds.emplace_back(url);
bool seeding = false;
error_code ignore;
remove("1", ignore);
run_test(
[&](lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::ssrf_mitigation, enable_feature);
ses.apply_settings(pack);
ses.async_add_torrent(params);
},
[&](lt::session&, lt::alert const* alert) {
if (lt::alert_cast<lt::torrent_finished_alert>(alert))
seeding = true;
},
[&](sim::simulation& sim, lt::session&)
{
// http1 is the root web server that will just redirect requests to
// other servers
sim::asio::io_context web_server1(sim, make_address_v4("2.2.2.2"));
sim::http_server http1(web_server1, 8080);
// redirect file 1 and file 2 to the same servers
if (redirect)
http1.register_redirect("/1", redirect);
// server for serving the content. This is on the local network
sim::asio::io_context web_server2(sim, make_address_v4("192.168.1.13"));
sim::http_server http2(web_server2, 8080);
serve_content_for(http2, "/1", fs, file_index_t(0));
serve_content_for(http2, "/1?query_string=1", fs, file_index_t(0));
sim.run();
}
);
return seeding;
}
TORRENT_TEST(ssrf_mitigation)
{
TEST_CHECK(test_ssrf("http://192.168.1.13:8080/1", nullptr, true));
TEST_CHECK(test_ssrf("http://192.168.1.13:8080/1", nullptr, false));
TEST_CHECK(test_ssrf("http://local-network.com:8080/1", nullptr, true));
TEST_CHECK(test_ssrf("http://local-network.com:8080/1", nullptr, false));
TEST_CHECK(!test_ssrf("http://192.168.1.13:8080/1?query_string=1", nullptr, true));
TEST_CHECK(test_ssrf("http://192.168.1.13:8080/1?query_string=1", nullptr, false));
TEST_CHECK(!test_ssrf("http://local-network.com:8080/1?query_string=1", nullptr, true));
TEST_CHECK(test_ssrf("http://local-network.com:8080/1?query_string=1", nullptr, false));
}
TORRENT_TEST(ssrf_mitigation_redirect)
{
TEST_CHECK(test_ssrf("http://2.2.2.2:8080/1", "http://192.168.1.13:8080/1", true));
TEST_CHECK(test_ssrf("http://2.2.2.2:8080/1", "http://192.168.1.13:8080/1", false));
TEST_CHECK(test_ssrf("http://2.2.2.2:8080/1", "http://local-network.com:8080/1", true));
TEST_CHECK(test_ssrf("http://2.2.2.2:8080/1", "http://local-network.com:8080/1", false));
TEST_CHECK(!test_ssrf("http://2.2.2.2:8080/1", "http://192.168.1.13:8080/1?query_string=1", true));
TEST_CHECK(test_ssrf("http://2.2.2.2:8080/1", "http://192.168.1.13:8080/1?query_string=1", false));
TEST_CHECK(!test_ssrf("http://2.2.2.2:8080/1", "http://local-network.com:8080/1?query_string=1", true));
TEST_CHECK(test_ssrf("http://2.2.2.2:8080/1", "http://local-network.com:8080/1?query_string=1", false));
}

View File

@ -1424,7 +1424,8 @@ namespace {
"privileged_ports",
"utp_disabled",
"tcp_disabled",
"invalid_local_interface"
"invalid_local_interface",
"ssrf_mitigation"
};
std::snprintf(ret, sizeof(ret), "%s: blocked peer [%s]"

View File

@ -71,6 +71,7 @@ http_connection::http_connection(io_context& ios
, int max_bottled_buffer_size
, http_connect_handler ch
, http_filter_handler fh
, hostname_filter_handler hfh
#if TORRENT_USE_SSL
, ssl::context* ssl_ctx
#endif
@ -87,6 +88,7 @@ http_connection::http_connection(io_context& ios
, m_handler(std::move(handler))
, m_connect_handler(std::move(ch))
, m_filter_handler(std::move(fh))
, m_hostname_filter_handler(std::move(hfh))
, m_timer(ios)
, m_completion_timeout(seconds(5))
, m_limiter_timer(ios)
@ -146,6 +148,14 @@ void http_connection::get(std::string const& url, time_duration timeout, int pri
return;
}
if (m_hostname_filter_handler && !m_hostname_filter_handler(*this, hostname))
{
error_code err(errors::banned_by_ip_filter);
post(m_ios, std::bind(&http_connection::callback
, me, err, span<char>{}));
return;
}
if (protocol != "http"
#if TORRENT_USE_SSL
&& protocol != "https"

View File

@ -215,6 +215,7 @@ namespace libtorrent {
, true, settings.get_int(settings_pack::max_http_recv_buffer_size)
, std::bind(&http_tracker_connection::on_connect, shared_from_this(), _1)
, std::bind(&http_tracker_connection::on_filter, shared_from_this(), _1, _2)
, std::bind(&http_tracker_connection::on_filter_hostname, shared_from_this(), _1, _2)
#if TORRENT_USE_SSL
, tracker_req().ssl_ctx
#endif
@ -295,7 +296,7 @@ namespace libtorrent {
}
aux::session_settings const& settings = m_man.settings();
bool const ssrf_mitigation = settings.get_bool(settings_pack::tracker_ssrf_mitigation);
bool const ssrf_mitigation = settings.get_bool(settings_pack::ssrf_mitigation);
if (ssrf_mitigation && std::find_if(endpoints.begin(), endpoints.end()
, [](tcp::endpoint const& ep) { return ep.address().is_loopback(); }) != endpoints.end())
{
@ -357,6 +358,15 @@ namespace libtorrent {
fail(errors::banned_by_ip_filter, operation_t::bittorrent);
}
// returns true if the hostname is allowed
bool http_tracker_connection::on_filter_hostname(http_connection&
, string_view hostname)
{
aux::session_settings const& settings = m_man.settings();
if (settings.get_bool(settings_pack::allow_idna)) return true;
return !is_idna(hostname);
}
void http_tracker_connection::on_connect(http_connection& c)
{
error_code ec;

View File

@ -175,4 +175,20 @@ exit:
return std::make_tuple(std::move(base), std::move(path));
}
TORRENT_EXTRA_EXPORT bool is_idna(string_view hostname)
{
for (;;)
{
auto dot = hostname.find('.');
string_view const label = (dot == string_view::npos) ? hostname : hostname.substr(0, dot);
if (label.size() >= 4
&& (label[0] == 'x' || label[0] == 'X')
&& (label[1] == 'n' || label[1] == 'N')
&& label.substr(2, 2) == "--"_sv)
return true;
if (dot == string_view::npos) return false;
hostname = hostname.substr(dot + 1);
}
}
}

View File

@ -216,8 +216,9 @@ namespace libtorrent {
SET(dht_ignore_dark_internet, true, nullptr),
SET(dht_read_only, false, nullptr),
SET(piece_extent_affinity, false, nullptr),
SET(validate_https_trackers, false, &session_impl::update_validate_https),
SET(tracker_ssrf_mitigation, true, nullptr),
SET(validate_https_trackers, true, &session_impl::update_validate_https),
SET(ssrf_mitigation, true, nullptr),
SET(allow_idna, false, nullptr),
SET(enable_set_file_valid_data, false, nullptr),
}});

View File

@ -6066,6 +6066,23 @@ namespace {
error_code ec;
std::tie(protocol, auth, hostname, port, path)
= parse_url_components(web->url, ec);
if (!settings().get_bool(settings_pack::allow_idna) && is_idna(hostname))
{
#ifndef TORRENT_DISABLE_LOGGING
if (should_log())
debug_log("IDNA disallowed in web seeds: %s", web->url.c_str());
#endif
if (m_ses.alerts().should_post<url_seed_alert>())
{
m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
, web->url, error_code(errors::banned_by_ip_filter));
}
// never try it again
remove_web_seed_iter(web);
return;
}
if (port == -1)
{
port = protocol == "http" ? 80 : 443;
@ -6403,9 +6420,10 @@ namespace {
}
std::string hostname;
std::string path;
error_code ec;
using std::ignore;
std::tie(ignore, ignore, hostname, ignore, ignore)
std::tie(ignore, ignore, hostname, ignore, path)
= parse_url_components(web->url, ec);
if (ec)
{
@ -6414,6 +6432,46 @@ namespace {
return;
}
if (!settings().get_bool(settings_pack::allow_idna) && is_idna(hostname))
{
#ifndef TORRENT_DISABLE_LOGGING
if (should_log())
debug_log("IDNA disallowed in web seeds: %s", web->url.c_str());
#endif
if (m_ses.alerts().should_post<url_seed_alert>())
{
m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
, web->url, error_code(errors::banned_by_ip_filter));
}
// never try it again
remove_web_seed_iter(web);
return;
}
// The SSRF mitigation for web seeds is that any HTTP server on the
// local network may not use any query string parameters
if (settings().get_bool(settings_pack::ssrf_mitigation)
&& aux::is_local(web->peer_info.addr)
&& path.find('?') != std::string::npos)
{
#ifndef TORRENT_DISABLE_LOGGING
if (should_log())
{
debug_log("*** SSRF MITIGATION BLOCKED WEB SEED: %s"
, web->url.c_str());
}
#endif
if (m_ses.alerts().should_post<url_seed_alert>())
m_ses.alerts().emplace_alert<url_seed_alert>(get_handle()
, web->url, errors::banned_by_ip_filter);
if (m_ses.alerts().should_post<peer_blocked_alert>())
m_ses.alerts().emplace_alert<peer_blocked_alert>(get_handle()
, a, peer_blocked_alert::ssrf_mitigation);
// never try it again
remove_web_seed_iter(web);
return;
}
bool const is_ip = aux::is_ip_address(hostname);
if (is_ip) a.address(make_address(hostname, ec));
bool const proxy_hostnames = settings().get_bool(settings_pack::proxy_hostnames)

View File

@ -440,6 +440,7 @@ void upnp::connect(rootdevice& d)
, std::ref(d), _4), true, default_max_bottled_buffer_size
, http_connect_handler()
, http_filter_handler()
, hostname_filter_handler()
#if TORRENT_USE_SSL
, &m_ssl_ctx
#endif
@ -852,6 +853,7 @@ void upnp::update_map(rootdevice& d, port_mapping_t const i)
, std::ref(d), i, _4), true, default_max_bottled_buffer_size
, std::bind(&upnp::create_port_mapping, self(), _1, std::ref(d), i)
, http_filter_handler()
, hostname_filter_handler()
#if TORRENT_USE_SSL
, &m_ssl_ctx
#endif
@ -869,6 +871,7 @@ void upnp::update_map(rootdevice& d, port_mapping_t const i)
, std::ref(d), i, _4), true, default_max_bottled_buffer_size
, std::bind(&upnp::delete_port_mapping, self(), std::ref(d), i)
, http_filter_handler()
, hostname_filter_handler()
#if TORRENT_USE_SSL
, &m_ssl_ctx
#endif
@ -1087,6 +1090,7 @@ void upnp::on_upnp_xml(error_code const& e
, std::ref(d), _4), true, default_max_bottled_buffer_size
, std::bind(&upnp::get_ip_address, self(), std::ref(d))
, http_filter_handler()
, hostname_filter_handler()
#if TORRENT_USE_SSL
, &m_ssl_ctx
#endif

View File

@ -133,6 +133,7 @@ void run_test(std::string const& url, int size, int status, int connected
std::shared_ptr<http_connection> h = std::make_shared<http_connection>(ios
, res, &::http_handler_test, true, 1024*1024, &::http_connect_handler_test
, http_filter_handler()
, hostname_filter_handler()
#if TORRENT_USE_SSL
, &ssl_ctx
#endif

View File

@ -779,3 +779,56 @@ TORRENT_TEST(invalid_chunk_3)
http_parser parser;
feed_bytes(parser, {reinterpret_cast<char const*>(invalid_chunked_input), sizeof(invalid_chunked_input)});
}
TORRENT_TEST(idna)
{
TEST_CHECK(!is_idna("a.b.com"));
TEST_CHECK(!is_idna("example.com"));
TEST_CHECK(!is_idna("exn--ample.com"));
// xn-- is the ACE introducer for a punycoded label. It can appear at the
// start of any label, but not in the middle (if it does, it's not
// interpreted as an ACE). Since hostnames are case insensitive, the ACE
// introducer has to be as well
TEST_CHECK(is_idna("xn--example.com"));
TEST_CHECK(is_idna("xN--example.com"));
TEST_CHECK(is_idna("xn--example-.com"));
TEST_CHECK(is_idna("subdomain.xn--example-.com"));
TEST_CHECK(is_idna("subdomain.example.xn--com-"));
// this isn't valid IDNA, but it's suspicious
TEST_CHECK(is_idna("xn--.com"));
// some weird edge-cases
TEST_CHECK(!is_idna(".............."));
TEST_CHECK(is_idna(".....xn--........."));
TEST_CHECK(is_idna(".....Xn--........."));
TEST_CHECK(is_idna(".....xN--........."));
TEST_CHECK(is_idna(".....XN--........."));
TEST_CHECK(!is_idna(".....-xn--........."));
TEST_CHECK(is_idna("xn--"));
TEST_CHECK(is_idna("Xn--"));
TEST_CHECK(is_idna("XN--"));
TEST_CHECK(is_idna("xN--"));
TEST_CHECK(is_idna("xn--.xn--"));
TEST_CHECK(is_idna(".....xn--"));
TEST_CHECK(is_idna("xn--..."));
TEST_CHECK(is_idna("xN--..."));
TEST_CHECK(!is_idna(""));
TEST_CHECK(!is_idna("."));
TEST_CHECK(!is_idna("x"));
TEST_CHECK(!is_idna("xn"));
TEST_CHECK(!is_idna("xn-"));
TEST_CHECK(!is_idna("-xn--"));
TEST_CHECK(!is_idna(".x"));
TEST_CHECK(!is_idna(".xn"));
TEST_CHECK(!is_idna(".xn-"));
TEST_CHECK(!is_idna(".-xn--"));
TEST_CHECK(!is_idna("x."));
TEST_CHECK(!is_idna("xn."));
TEST_CHECK(!is_idna("xn-."));
TEST_CHECK(!is_idna("-xn--."));
}

View File

@ -100,6 +100,11 @@ void test_transfer(lt::session& ses, std::shared_ptr<torrent_info> torrent_file
, keepalive ? "yes" : "no");
int proxy_port = 0;
settings_pack pack;
// we use a self-signed cert for HTTPS trackers, the test would fail if we
// tried to validate it.
if (protocol == "https"_sv)
pack.set_bool(settings_pack::validate_https_trackers, false);
if (proxy)
{
proxy_port = start_proxy(proxy);
@ -108,26 +113,23 @@ void test_transfer(lt::session& ses, std::shared_ptr<torrent_info> torrent_file
std::printf("failed to start proxy");
return;
}
settings_pack pack;
pack.set_str(settings_pack::proxy_hostname, "127.0.0.1");
pack.set_str(settings_pack::proxy_username, "testuser");
pack.set_str(settings_pack::proxy_password, "testpass");
pack.set_int(settings_pack::proxy_type, proxy);
pack.set_int(settings_pack::proxy_port, proxy_port);
pack.set_bool(settings_pack::proxy_peer_connections, proxy_peers);
ses.apply_settings(pack);
}
else
{
settings_pack pack;
pack.set_str(settings_pack::proxy_hostname, "");
pack.set_str(settings_pack::proxy_username, "");
pack.set_str(settings_pack::proxy_password, "");
pack.set_int(settings_pack::proxy_type, settings_pack::none);
pack.set_int(settings_pack::proxy_port, 0);
pack.set_bool(settings_pack::proxy_peer_connections, proxy_peers);
ses.apply_settings(pack);
}
ses.apply_settings(pack);
add_torrent_params p;
p.flags &= ~torrent_flags::paused;