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