1 /*
2  * UrlPorts.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include <boost/random/mersenne_twister.hpp>
17 #include <boost/random/uniform_int.hpp>
18 #include <boost/random/variate_generator.hpp>
19 #include <boost/format.hpp>
20 
21 #include <server_core/UrlPorts.hpp>
22 #include <shared_core/SafeConvert.hpp>
23 #include <core/RegexUtils.hpp>
24 #include <iomanip>
25 
26 /* Port transformation is done in order to obscure port values in portmapped URLs. This improves
27  * privacy, and makes it more difficult to try to connect to a local service as someone else. The
28  * process works as follows:
29  *
30  * 1. On login, a cookie is set with 6 random bytes, which form the token for the transformation.
31  *    The first two form the multiplier, and the last four are the key.
32  *
33  *    e.g. port-token=a433e59dc087 => multiplier a433, key e59dc087
34  *
35  * 2. To build an obscured port (for constructing a URL), the 6 bytes of the token are mixed with
36  *    the 2 byte port value to create an obscured 4-byte (plus server nibble) port value, as follows:
37  *
38  *    a. The port number is sent through a modular multiplicative inverse. This doesn't add any
39  *       security; it just obscures common ports in 2-byte space.
40  *
41  *    b. The 2-byte value is multiplied by the 2-byte multiplier to get a 4-byte value.
42  *
43  *    c. The 4 byte value is XOR'ed with the key to form the final 4-byte obscured value.
44  *
45  *    d. If the port to be obscured is a server port (as opposed to a session port), an
46  *       additional nibble based on the first digit of the key is prefixed to the obscured value.
47  *
48  *    e. A URL is formed using a hex-encoded version of the value, e.g. /p/58fab3e4.
49  *
50  * 3. When processing portmapped URLs, the value from the port-token cookie is used to run the
51  *    algorithm above in reverse to recover the raw port value.
52  *
53  * Note that this system is NOT CRYPTOGRAPHICALLY SECURE; in particular, if it's possible to observe
54  * many obscured values from the same session, information about that session's token can be
55  * inferred. This system is designed only to prevent casual attempts to abuse portmapped URLs by
56  * making them user-specific and difficult to predict without prior knowledge. Any web service
57  * running on the same host as RStudio Server should implement best practices for cross-site request
58  * forgery (CSRF).
59  */
60 
61 #define TRANSFORM_PORT(x)   ((x * 8854)  % 65535)
62 #define DETRANSFORM_PORT(x) ((x * 61279) % 65535)
63 
64 namespace rstudio {
65 namespace server_core {
66 namespace {
67 
splitToken(const std::string & token,uint32_t * pMultiplier,uint64_t * pKey)68 bool splitToken(const std::string& token, uint32_t* pMultiplier, uint64_t* pKey)
69 {
70    try
71    {
72       // split the token into the multiplier and the key
73       *pMultiplier = std::stoi(token.substr(0, 4), nullptr, 16);
74       *pKey = std::stoul(token.substr(4), nullptr, 16);
75       return true;
76    }
77    CATCH_UNEXPECTED_EXCEPTION
78 
79    return false;
80 }
81 
82 } // anonymous namespace
83 
transformPort(const std::string & token,int port,bool server)84 std::string transformPort(const std::string& token, int port, bool server)
85 {
86    uint32_t multiplier;
87    uint64_t key;
88    if (splitToken(token, &multiplier, &key))
89    {
90       // transform, multiply, xor, and return
91       uint64_t result = ((TRANSFORM_PORT(port) * multiplier) ^ key);
92       if (server)
93       {
94          // obtain the lower 4 bits (nibble) of the key's 1st byte
95          // if zero, assume it to be at least one - otherwise when
96          // formatted a leading zero wouldn't show up!
97          // use this 9th digit to indicate server routing
98          uint64_t nibble = ((key << 8) & 0xF00000000);
99          result |= std::max(uint64_t(0x100000000), nibble);
100       }
101       return (boost::format("%08x") % result).str();
102    }
103    else
104    {
105       LOG_ERROR_MESSAGE("Cannot create URL with port token '" + token + "'.");
106    }
107 
108    // return empty string for unexpected port
109    return std::string();
110 }
111 
detransformPort(const std::string & token,const std::string & port,bool & server)112 int detransformPort(const std::string& token, const std::string& port, bool& server)
113 {
114    uint32_t multiplier;
115    uint64_t key;
116    if (splitToken(token, &multiplier, &key))
117    {
118       try
119       {
120          // xor, divide, de-transform, and return
121          uint64_t result = std::stoull(port, nullptr, 16);
122 
123          // a value over 8 hex digits long indicate a server routing
124          server = result > 0xFFFFFFFF;
125          if (server)
126          {
127             // the 9th digit (prefix) should be the lower
128             // 4 bits of the key's 1st byte. If zero, it
129             // should be at least one (to show up in the
130             // formatted port string).
131             uint64_t nibble = (key & 0x0F000000);
132             if (nibble == 0)
133                nibble = 0x01000000;
134 
135             // fails if the incoming value prefix doesn't match the key
136             if (nibble != ((result & 0xF00000000) >> 8))
137             {
138                LOG_ERROR_MESSAGE("Invalid indicator on port token '" + token + "'.");
139                return -1;
140             }
141             result &= 0xFFFFFFFF;
142          }
143          return DETRANSFORM_PORT((result ^ key) / multiplier);
144       }
145       CATCH_UNEXPECTED_EXCEPTION
146    }
147    else
148    {
149       LOG_ERROR_MESSAGE("Cannot use port token '" + token + "'.");
150    }
151 
152    // return invalid port on failure
153    return -1;
154 }
155 
generateNewPortToken()156 std::string generateNewPortToken()
157 {
158    // configure random number generation for the token
159    static boost::mt19937 gen(std::time(nullptr));
160    boost::uniform_int<> dist(1, 65535);  // Avoid zeroes since we'll be multiplying and XORing
161    boost::variate_generator<boost::mt19937&, boost::uniform_int<> > var(gen, dist);
162    std::vector<uint32_t> token;
163 
164    // create 2 random bytes for the multiplier
165    token.push_back(var());
166 
167    // create 4 random bytes for the key
168    token.push_back(var());
169    token.push_back(var());
170 
171    // convert to hex and format as a token
172    std::ostringstream ostr;
173    for (auto t: token)
174       ostr << std::setw(4) << std::setfill('0') << std::hex << static_cast<uint32_t>(t);
175 
176    return ostr.str();
177 }
178 
portmapPathForLocalhostUrl(const std::string & url,const std::string & token,std::string * pPath)179 bool portmapPathForLocalhostUrl(const std::string& url, const std::string& token,
180       std::string* pPath)
181 {
182    // match an http URL (ipv4 localhost or ipv6 localhst) and extract the port
183    boost::regex re("http[s]?://(?:localhost|127\\.0\\.0\\.1|::1|\\[::1\\]):([0-9]+)(/.*)?");
184    boost::smatch match;
185    if (core::regex_utils::search(url, match, re))
186    {
187       bool ipv6 = (url.find("::1") != std::string::npos);
188 
189       // calculate the path
190       std::string path = match[2];
191       if (path.empty())
192          path = "/";
193       std::string portPath = ipv6 ? "p6/" : "p/";
194 
195       // convert port
196       auto port = core::safe_convert::stringTo<int>(match[1]);
197       if (!port)
198          return false;
199 
200       // create and return computed path
201       path = portPath +
202              transformPort(token, *port) +
203              path;
204       *pPath = path;
205       return true;
206    }
207    else
208    {
209       return false;
210    }
211 }
212 
213 }  // namespace server_core
214 }  // namespace rstudio
215