Skip to content

Commit 77aea55

Browse files
authored
A couple more CSP enhancements (#2530)
- Configure object-src for Knitr tests and remove exclusion from `CspLogUtil` - Add regression test for script nonce in report webpart (`AbstractKnitrReportTest`) - Remove redundant methods from `PortalHelper` - Move some methods from `WikiHelper` to `wiki.EditPage`
1 parent f46ec8a commit 77aea55

File tree

15 files changed

+187
-50
lines changed

15 files changed

+187
-50
lines changed

data/reports/knitr_no_scriptpad.rhtml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,11 @@ end.rcode-->
9393
<p>Well, everything seems to be working. Let's ask R what is the
9494
value of &pi;? Of course it is <!--rinline pi -->.</p>
9595

96+
<span>Nonce check: <span id="nonce-check-result">FAIL</span></span>
97+
98+
<script>
99+
document.getElementById('nonce-check-result').innerText = "SUCCESS";
100+
</script>
101+
96102
</body>
97103
</html>

data/reports/knitr_no_scriptpad.rmd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ library(knitr)
113113
knit('knitr-minimal.Rmd')
114114
```
115115

116+
<span>Nonce check: <span id="nonce-check-result">FAIL</span></span>
117+
118+
<script>
119+
document.getElementById('nonce-check-result').innerText = "SUCCESS";
120+
</script>
121+
116122
## Conclusion
117123

118124
Markdown is super easy to write. Go to **knitr** [homepage](http://yihui.name/knitr) for details.

data/reports/nonce_check.rhtml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Test script nonce in Knitr HTML</title>
5+
</head>
6+
<body>
7+
8+
<span>Nonce check: <span id="nonce-check-result">FAIL</span></span>
9+
10+
<script>
11+
document.getElementById('nonce-check-result').innerText = "SUCCESS";
12+
</script>
13+
14+
</body>
15+
</html>

modules/scriptpad/resources/reports/schemas/script_rhtml.rhtml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,11 @@ end.rcode-->
8787
<p>Well, everything seems to be working. Let's ask R what is the
8888
value of &pi;? Of course it is <!--rinline pi -->.</p>
8989

90+
<span>Nonce check: <span id="nonce-check-result">FAIL</span></span>
91+
92+
<script>
93+
document.getElementById('nonce-check-result').innerText = "SUCCESS";
94+
</script>
95+
9096
</body>
9197
</html>

modules/scriptpad/resources/reports/schemas/script_rmd.rmd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ library(knitr)
108108
knit('knitr-minimal.Rmd')
109109
```
110110

111+
<span>Nonce check: <span id="nonce-check-result">FAIL</span></span>
112+
113+
<script>
114+
document.getElementById('nonce-check-result').innerText = "SUCCESS";
115+
</script>
116+
111117
## Conclusion
112118

113119
Markdown is super easy to write. Go to **knitr** [homepage](http://yihui.name/knitr) for details.

src/org/labkey/test/TestFileUtils.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static String getFileContents(Path path)
109109
{
110110
try
111111
{
112-
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
112+
return Files.readString(path);
113113
}
114114
catch (IOException fail)
115115
{
@@ -306,7 +306,7 @@ public static Set<File> getSampleDataDirs()
306306
if (sampledataDirsFile.exists())
307307
{
308308
String path = getFileContents(sampledataDirsFile);
309-
_sampledataDirs.addAll(Arrays.stream(path.split(";")).map(File::new).collect(Collectors.toList()));
309+
_sampledataDirs.addAll(Arrays.stream(path.split(";")).map(File::new).toList());
310310
}
311311
else
312312
{
@@ -317,7 +317,7 @@ public static Set<File> getSampleDataDirs()
317317
// We know where the modules live; no reason to insist that sampledata.dirs exists.
318318
Files.walkFileTree(modulesDir, Collections.emptySet(), 2, new SimpleFileVisitor<>(){
319319
@Override
320-
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
320+
public @NotNull FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAttributes attrs)
321321
{
322322
if (dir.equals(modulesDir))
323323
{

src/org/labkey/test/pages/admin/ExternalSourcesPage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ public enum Directive implements OptionSelect.SelectOption
207207
Frame("frame-src"),
208208
Image("image-src"),
209209
Style("style-src"),
210+
Object("object-src"),
210211
;
211212

212213
private final String directiveId;

src/org/labkey/test/pages/reports/ScriptReportPage.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
import org.labkey.test.pages.LabKeyPage;
1212
import org.labkey.test.util.CodeMirrorHelper;
1313
import org.labkey.test.util.Ext4Helper;
14+
import org.labkey.test.util.PipelineStatusTable;
1415
import org.labkey.test.util.TestLogger;
1516
import org.openqa.selenium.WebDriver;
1617
import org.openqa.selenium.WebElement;
1718
import org.openqa.selenium.support.ui.ExpectedConditions;
1819

1920
import java.util.Map;
21+
import java.util.Objects;
2022
import java.util.Optional;
2123

2224
import static org.labkey.test.components.ext4.Checkbox.Ext4Checkbox;
@@ -79,13 +81,19 @@ public CodeMirrorHelper getEditor()
7981

8082
public String saveReport(String name, boolean isSaveAs, int wait)
8183
{
84+
String reportIdBeforeSave = getReportId();
8285
WebElement saveButton = Ext4Helper.Locators.ext4Button(isSaveAs ? "Save As" : "Save").findElement(getDriver());
8386
scrollIntoView(saveButton, true);
8487
clickAndWait(saveButton, wait);
8588
if (null != name)
8689
{
8790
saveReportWithName(name, isSaveAs);
8891
}
92+
return Objects.requireNonNullElse(getReportId(), reportIdBeforeSave);
93+
}
94+
95+
public String getReportId()
96+
{
8997
return getUrlParam("reportId", true);
9098
}
9199

@@ -141,6 +149,7 @@ public void clearOption(ReportOption option)
141149

142150
private void _selectOption(ReportOption option, boolean checked)
143151
{
152+
clickSourceTab();
144153
ensureFieldSetExpanded(option.getSection());
145154
Checkbox checkbox;
146155
if (option.isCheckbox())
@@ -202,6 +211,17 @@ public WebElement findReportElement()
202211
return Locator.byClass("reportView").findElement(getDriver());
203212
}
204213

214+
public void startPipelineJobAndWait()
215+
{
216+
clickReportTab();
217+
218+
waitAndClick(Locator.lkButton("Start Job"));
219+
waitAndClickAndWait(Locator.linkWithText("click here"));
220+
new PipelineStatusTable(this)
221+
.clickStatusLink(0)
222+
.waitForComplete();
223+
}
224+
205225
@Override
206226
protected ElementCache newElementCache()
207227
{

src/org/labkey/test/pages/wiki/EditPage.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,48 @@ public EditPage setTitle(String title)
100100

101101
public EditPage setBody(String body)
102102
{
103+
switchWikiToSourceView();
103104
elementCache().bodyTextArea.set(body);
104105
return this;
105106
}
106107

108+
/**
109+
* Switches the wiki edit page to source view when the format type is HTML.
110+
*/
111+
public void switchWikiToSourceView()
112+
{
113+
String curFormat = executeScript("return LABKEY._wiki.getProps().rendererType;", String.class);
114+
if (curFormat.equalsIgnoreCase("HTML"))
115+
{
116+
if (isElementPresent(Locator.css("#wiki-tab-source.labkey-tab-inactive")))
117+
{
118+
Locator tab = Locator.css("#wiki-tab-source > a");
119+
waitForElementToBeVisible(tab);
120+
click(tab);
121+
waitForElement(Locator.css("#wiki-tab-source.labkey-tab-active"));
122+
}
123+
}
124+
}
125+
126+
public void switchWikiToVisualView()
127+
{
128+
String curFormat = (String) executeScript("return LABKEY._wiki.getProps().rendererType;");
129+
if (curFormat.equalsIgnoreCase("HTML"))
130+
{
131+
if (isElementPresent(Locator.css("#wiki-tab-visual.labkey-tab-inactive")))
132+
{
133+
Locator tab = Locator.css("#wiki-tab-visual > a");
134+
waitForElementToBeVisible(tab);
135+
click(tab);
136+
137+
Locator yesButton = Locator.tagWithText("span","Yes");
138+
waitForElementToBeVisible(yesButton);
139+
waitAndClick(yesButton);
140+
waitForElement(Locator.css("#wiki-tab-visual.labkey-tab-active"));
141+
}
142+
}
143+
}
144+
107145
public EditPage setShouldIndex(boolean shouldIndex)
108146
{
109147
elementCache().shouldIndexCheckbox.set(shouldIndex);

src/org/labkey/test/tests/AbstractKnitrReportTest.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.junit.Assume;
1919
import org.junit.BeforeClass;
20+
import org.junit.Test;
2021
import org.labkey.remoteapi.CommandException;
2122
import org.labkey.test.BaseWebDriverTest;
2223
import org.labkey.test.Locator;
@@ -25,12 +26,16 @@
2526
import org.labkey.test.pages.admin.ExternalSourcesPage;
2627
import org.labkey.test.pages.core.admin.logger.ManagerPage;
2728
import org.labkey.test.pages.reports.ManageViewsPage;
29+
import org.labkey.test.pages.reports.ScriptReportPage;
30+
import org.labkey.test.pages.reports.ScriptReportPage.StandardReportOption;
2831
import org.labkey.test.util.CodeMirrorHelper;
32+
import org.labkey.test.util.CspLogUtil;
2933
import org.labkey.test.util.Log4jUtils;
3034
import org.labkey.test.util.LogMethod;
3135
import org.labkey.test.util.LoggedParam;
3236
import org.labkey.test.util.PortalHelper;
3337
import org.labkey.test.util.RReportHelper;
38+
import org.labkey.test.util.WikiHelper;
3439
import org.labkey.test.util.core.admin.CspConfigHelper;
3540
import org.openqa.selenium.WebElement;
3641

@@ -54,6 +59,10 @@ public abstract class AbstractKnitrReportTest extends BaseWebDriverTest
5459
protected static final Path rmdReport_no_scriptpad = TestFileUtils.getSampleData("reports/knitr_no_scriptpad.rmd").toPath();
5560
private static final Path rhtmlReport = scriptpadReports.resolve("script_rhtml.rhtml");
5661
private static final Path rhtmlReport_no_scriptpad = TestFileUtils.getSampleData("reports/knitr_no_scriptpad.rhtml").toPath();
62+
private static final Path rhtmlNonceCheck = TestFileUtils.getSampleData("reports/nonce_check.rhtml").toPath();
63+
private static final Locator.XPathLocator nonceCheckLoc = Locator.id("nonce-check-result");
64+
private static final Locator.XPathLocator nonceCheckSuccessLoc = nonceCheckLoc.withText("SUCCESS");
65+
5766
protected final RReportHelper _rReportHelper = new RReportHelper(this);
5867

5968
private static String readReport(final Path reportFile)
@@ -81,6 +90,7 @@ protected void setupProject()
8190
try
8291
{
8392
new CspConfigHelper(this).setAllowedHosts(Map.of(
93+
ExternalSourcesPage.Directive.Object, List.of("'self'"), // Issue 53226: reports-streamFile is blocked by object-src CSP directive
8494
ExternalSourcesPage.Directive.Style, List.of("https://cdn.datatables.net"),
8595
ExternalSourcesPage.Directive.Font, List.of("https://mathjax.rstudio.com")));
8696
}
@@ -166,7 +176,9 @@ protected void htmlFormat()
166176
Locator.tag("pre").containing("## \"1\",249318596,\"2008-05-17\",86,36,129,76,64"),
167177
Locator.tag("pre").withText("## knitr says hello to HTML!"),
168178
Locator.tag("pre").startsWith("## Error").containing(": non-numeric argument to binary operator"),
169-
Locator.tag("p").startsWith("Well, everything seems to be working. Let's ask R what is the value of \u03C0? Of course it is 3.141")};
179+
Locator.tag("p").startsWith("Well, everything seems to be working. Let's ask R what is the value of \u03C0? Of course it is 3.141"),
180+
nonceCheckSuccessLoc // Inline script should run
181+
};
170182
String[] reportNotContains = {"<html>", // Uninterpreted html
171183
"<!--", // ditto
172184
"A minimal knitr example in HTML", // report title element
@@ -195,7 +207,8 @@ protected void markdownV2()
195207
Locator.tag("h2").withText("R code chunks"),
196208
Locator.tag("code").containing("set.seed(123)"), // Echoed R code
197209
Locator.css("p").containing("2 x pi = 6.283"),
198-
Locator.tag("sup").withText("write") //should not contain the hat markdown v2 closing tag
210+
Locator.tag("sup").withText("write"), //should not contain the hat markdown v2 closing tag
211+
nonceCheckSuccessLoc // Inline script should run
199212
};
200213

201214
String[] reportNotContains = {"```", // Markdown for R code chunks
@@ -224,4 +237,66 @@ protected void moduleReportDependencies()
224237
_ext4Helper.waitForMaskToDisappear(3 * BaseWebDriverTest.WAIT_FOR_JAVASCRIPT);
225238
waitForElement(Locator.id("mtcars_table"));
226239
}
240+
241+
/**
242+
* Issue 53211: CSP reports when an R/Plotly graph is displayed in Reports web part, same thing wrapped in a wiki works fine with strict csp
243+
*/
244+
@Test
245+
public void testEmbeddedReportNonce()
246+
{
247+
CspConfigHelper.debugCspWarnings();
248+
new CspConfigHelper(this).setEnforceCsp(false);
249+
250+
String name = "rhtml nonce check";
251+
Locator[] reportContains = {nonceCheckSuccessLoc};
252+
253+
createAndVerifyKnitrReport(rhtmlNonceCheck, RReportHelper.ReportOption.knitrHtml, reportContains,
254+
null, true, name);
255+
assertNonceSuccess();
256+
257+
log("Create wiki with embedded report");
258+
new WikiHelper(this).createNewWikiPage()
259+
.setName(name)
260+
.setBody("""
261+
${labkey.webPart(partName='Report',
262+
reportName='%s',
263+
showFrame='false'
264+
)}
265+
""".formatted(name))
266+
.saveAndClose();
267+
clickAndWait(Locator.linkWithText(name));
268+
assertNonceSuccess();
269+
270+
log("Add report webpart");
271+
new PortalHelper(this).doInAdminMode(ph -> {
272+
ph.addTab(name + " tab"); // Use a separate tab to ensure report isn't run accidentally
273+
ph.addReportWebPart(name);
274+
assertNonceSuccess();
275+
});
276+
277+
log("Re-verify with report run as pipeline job");
278+
goToManageViews();
279+
waitAndClickAndWait(Locator.linkWithText(name));
280+
ScriptReportPage reportPage = _rReportHelper.getReportPage();
281+
reportPage.selectOption(StandardReportOption.runInPipeline);
282+
String reportId = reportPage.saveReport(null, false, WAIT_FOR_PAGE);
283+
goBack();
284+
reportPage.startPipelineJobAndWait();
285+
286+
ScriptReportPage.beginAtReport(this, getCurrentContainerPath(), reportId);
287+
assertNonceSuccess();
288+
289+
clickTab(name + " tab");
290+
assertNonceSuccess();
291+
292+
goToModule("Wiki");
293+
clickAndWait(Locator.linkWithText(name));
294+
assertNonceSuccess();
295+
}
296+
297+
private void assertNonceSuccess()
298+
{
299+
assertEquals("Nonce check result", "SUCCESS", getText(nonceCheckLoc));
300+
CspLogUtil.checkNewCspWarnings(getArtifactCollector());
301+
}
227302
}

0 commit comments

Comments
 (0)