Skip to content

Commit 28418f5

Browse files
committed
[r] Sanitize R package and repo names in TRInterface helpers
`TRInterface::IsInstalled`, `::Require`, and `::Install` built R source by concatenating the caller-provided package name into a string literal and passing it to the embedded R interpreter. Validate pkg against CRAN's package-name rule (starts with an ASCII letter, only letters, digits, and dots, does not end in a dot) before putting it into the R command, and emit an Error and return `kFALSE` on any other input. The `repos` argument of Install is validated to be a valid URL. (cherry picked from commit 12173d5)
1 parent e529f6a commit 28418f5

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

bindings/r/src/TRInterface.cxx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,23 +188,90 @@ TRInterface &TRInterface::Instance()
188188
return *TRInterface::InstancePtr();
189189
}
190190

191+
namespace {
192+
193+
// Per CRAN policy, an R package name starts with a letter, contains only ASCII
194+
// letters, digits, and dots, and does not end with a dot. Restricting to this
195+
// set is a helpful validation step for the user and prevents R-source
196+
// injection via the string concatenation done in IsInstalled / Require /
197+
// Install below.
198+
bool IsValidRPackageName(const TString &pkg)
199+
{
200+
const Ssiz_t n = pkg.Length();
201+
if (n == 0)
202+
return false;
203+
const char first = pkg[0];
204+
if (!((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')))
205+
return false;
206+
for (Ssiz_t i = 1; i < n; ++i) {
207+
const char c = pkg[i];
208+
const Bool_t ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.';
209+
if (!ok)
210+
return false;
211+
}
212+
return pkg[n - 1] != '.';
213+
}
214+
215+
// Allow only an http(s)/ftp/file URL with no characters that could break
216+
// out of the R single-quoted literal in Install().
217+
bool IsValidRReposUrl(const TString &repos)
218+
{
219+
const Ssiz_t n = repos.Length();
220+
if (n == 0)
221+
return false;
222+
const char *prefixes[] = {"http://", "https://", "ftp://", "file://"};
223+
Bool_t prefixOk = false;
224+
for (const char *s : prefixes) {
225+
if (repos.BeginsWith(s)) {
226+
prefixOk = true;
227+
break;
228+
}
229+
}
230+
if (!prefixOk)
231+
return false;
232+
for (Ssiz_t i = 0; i < n; ++i) {
233+
const char c = repos[i];
234+
if (c == '\'' || c == '\\' || c == '`' || c == ';' || c == '\n' || c == '\r')
235+
return false;
236+
}
237+
return true;
238+
}
239+
240+
} // namespace
241+
191242
//______________________________________________________________________________
192243
Bool_t TRInterface::IsInstalled(TString pkg)
193244
{
245+
if (!IsValidRPackageName(pkg)) {
246+
Error("IsInstalled", "Invalid R package name: %s", pkg.Data());
247+
return kFALSE;
248+
}
194249
TString cmd = "is.element('" + pkg + "', installed.packages()[,1])";
195250
return this->Eval(cmd).As<Bool_t>();
196251
}
197252

198253
//______________________________________________________________________________
199254
Bool_t TRInterface::Require(TString pkg)
200255
{
256+
if (!IsValidRPackageName(pkg)) {
257+
Error("Require", "Invalid R package name: %s", pkg.Data());
258+
return kFALSE;
259+
}
201260
TString cmd = "require('" + pkg + "',quiet=TRUE)";
202261
return this->Eval(cmd).As<Bool_t>();
203262
}
204263

205264
//______________________________________________________________________________
206265
Bool_t TRInterface::Install(TString pkg, TString repos)
207266
{
267+
if (!IsValidRPackageName(pkg)) {
268+
Error("Install", "Invalid R package name: %s", pkg.Data());
269+
return kFALSE;
270+
}
271+
if (!IsValidRReposUrl(repos)) {
272+
Error("Install", "Invalid R repository URL: %s", repos.Data());
273+
return kFALSE;
274+
}
208275
TString cmd = "install.packages('" + pkg + "',repos='" + repos + "',dependencies=TRUE)";
209276
this->Eval(cmd);
210277
return IsInstalled(pkg);

0 commit comments

Comments
 (0)