Skip to content

Commit

Permalink
Add security improvement - you can (and should) define a list of allo…
Browse files Browse the repository at this point in the history
…wed url prefixes for the REST api to prevent users to call malicious urls
  • Loading branch information
MediaMarco committed Aug 7, 2024
1 parent 43141e6 commit 8819fe3
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 4 deletions.
5 changes: 5 additions & 0 deletions web/src/main/java/de/otto/jlineup/web/JLineupController.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,9 @@ public ResponseEntity<String> exceptionHandler(final InvalidDefinitionException
public ResponseEntity<String> exceptionHandler(final ValidationError exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> exceptionHandler(final IllegalArgumentException exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY);
}
}
11 changes: 10 additions & 1 deletion web/src/main/java/de/otto/jlineup/web/JLineupRunnerFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.otto.jlineup.RunStepConfig;
import de.otto.jlineup.config.JobConfig;
import de.otto.jlineup.config.RunStep;
import de.otto.jlineup.config.UrlConfig;
import de.otto.jlineup.service.BrowserNotInstalledException;
import de.otto.jlineup.web.configuration.JLineupWebProperties;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -60,12 +61,20 @@ private JLineupRunner createRun(String id, JobConfig jobConfig, RunStep step) th
.build());
}

JobConfig sanitizeJobConfig(final JobConfig jobConfig) throws BrowserNotInstalledException {
JobConfig sanitizeJobConfig(final JobConfig jobConfig) throws BrowserNotInstalledException, IllegalArgumentException {

if (!properties.getInstalledBrowsers().contains(jobConfig.browser)) {
throw new BrowserNotInstalledException(jobConfig.browser);
}

if (!properties.getAllowedUrlPrefixes().isEmpty()) {
for (UrlConfig urlConfig : jobConfig.urls.values()) {
if (properties.getAllowedUrlPrefixes().stream().noneMatch(urlConfig.getUrl()::startsWith)) {
throw new IllegalArgumentException("URL " + urlConfig.url + " is not allowed. Allowed prefixes are: " + properties.getAllowedUrlPrefixes());
}
}
}

return JobConfig.copyOfBuilder(jobConfig)
.withThreads(calculateNumberOfThreads(jobConfig))
.withDebug(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public String toString() {
", chromeLaunchParameters=" + chromeLaunchParameters +
", firefoxLaunchParameters=" + firefoxLaunchParameters +
", installedBrowsers=" + installedBrowsers +
", allowedUrlPrefixes=" + allowedUrlPrefixes +
", maxPersistedRuns=" + maxPersistedRuns +
", lambda=" + lambda +
'}';
Expand All @@ -52,14 +53,16 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JLineupWebProperties that = (JLineupWebProperties) o;
return cleanupProfile == that.cleanupProfile && maxParallelJobs == that.maxParallelJobs && maxThreadsPerJob == that.maxThreadsPerJob && maxPersistedRuns == that.maxPersistedRuns && Objects.equals(workingDirectory, that.workingDirectory) && Objects.equals(screenshotsDirectory, that.screenshotsDirectory) && Objects.equals(reportDirectory, that.reportDirectory) && Objects.equals(chromeLaunchParameters, that.chromeLaunchParameters) && Objects.equals(firefoxLaunchParameters, that.firefoxLaunchParameters) && Objects.equals(installedBrowsers, that.installedBrowsers) && Objects.equals(lambda, that.lambda);
return cleanupProfile == that.cleanupProfile && maxParallelJobs == that.maxParallelJobs && maxThreadsPerJob == that.maxThreadsPerJob && maxPersistedRuns == that.maxPersistedRuns && Objects.equals(workingDirectory, that.workingDirectory) && Objects.equals(screenshotsDirectory, that.screenshotsDirectory) && Objects.equals(reportDirectory, that.reportDirectory) && Objects.equals(chromeLaunchParameters, that.chromeLaunchParameters) && Objects.equals(firefoxLaunchParameters, that.firefoxLaunchParameters) && Objects.equals(installedBrowsers, that.installedBrowsers) && Objects.equals(allowedUrlPrefixes, that.allowedUrlPrefixes) && Objects.equals(lambda, that.lambda);
}

@Override
public int hashCode() {
return Objects.hash(workingDirectory, screenshotsDirectory, reportDirectory, cleanupProfile, maxParallelJobs, maxThreadsPerJob, chromeLaunchParameters, firefoxLaunchParameters, installedBrowsers, maxPersistedRuns, lambda);
return Objects.hash(workingDirectory, screenshotsDirectory, reportDirectory, cleanupProfile, maxParallelJobs, maxThreadsPerJob, chromeLaunchParameters, firefoxLaunchParameters, installedBrowsers, allowedUrlPrefixes, maxPersistedRuns, lambda);
}

private List<String> allowedUrlPrefixes = emptyList();

private int maxPersistedRuns = DEFAULT_MAX_PERSISTED_RUNS;

private JLineupWebLambdaProperties lambda = new JLineupWebLambdaProperties();
Expand Down Expand Up @@ -151,4 +154,12 @@ public int getMaxPersistedRuns() {
public void setMaxPersistedRuns(int maxPersistedRuns) {
this.maxPersistedRuns = maxPersistedRuns;
}

public List<String> getAllowedUrlPrefixes() {
return allowedUrlPrefixes;
}

public void setAllowedUrlPrefixes(List<String> allowedUrlPrefixes) {
this.allowedUrlPrefixes = allowedUrlPrefixes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"properties": [
{
"name": "jlineup.allowedUrlPrefixes",
"type": "java.util.List<java.lang.String>",
"description": "All URLs that you tell JLineup to call via REST api have to begin with one of these prefixes. This is a security feature to prevent JLineup from calling arbitrary URLs."
}
] }
4 changes: 4 additions & 0 deletions web/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ jlineup:
maxParallelJobs: 1
chromeLaunchParameters: --use-spdy=off, --disable-dev-shm-usage
cleanupProfile: true
allowedUrlPrefixes:
- https://www.otto.de
- https://develop.otto.de
- https://www.example.com
lambda:
functionName: jlineup-lambda

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package de.otto.jlineup.web;

import de.otto.jlineup.config.JobConfig;
import de.otto.jlineup.config.UrlConfig;
import de.otto.jlineup.service.BrowserNotInstalledException;
import de.otto.jlineup.web.configuration.JLineupWebProperties;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static de.otto.jlineup.browser.Browser.Type.*;
import static de.otto.jlineup.config.JobConfig.*;
Expand All @@ -23,11 +26,12 @@ public void setup() {
jLineupWebProperties = new JLineupWebProperties();
jLineupWebProperties.setInstalledBrowsers(Arrays.asList(CHROME, CHROME_HEADLESS));
jLineupWebProperties.setMaxThreadsPerJob(10);
jLineupWebProperties.setAllowedUrlPrefixes(List.of("https://www.example.com"));
jLineupRunnerFactory = new JLineupRunnerFactory(jLineupWebProperties);
}

@Test
public void shouldSanitizeJobConfig() throws BrowserNotInstalledException {
public void shouldSanitizeJobConfigForInstalledBrowsers() throws BrowserNotInstalledException {

//Given
JobConfig evilJobConfig = copyOfBuilder(exampleConfig())
Expand Down Expand Up @@ -59,6 +63,30 @@ public void shouldThrowBrowserNotInstalledException() throws BrowserNotInstalled
jLineupRunnerFactory.sanitizeJobConfig(jobConfig);
}

@Test(expected = IllegalArgumentException.class)
public void shouldThrowIllegalArgumentExceptionBecauseURLIsNotAllowed() throws IllegalArgumentException, BrowserNotInstalledException {
// Given
JobConfig jobConfig = copyOfBuilder(exampleConfig())
.withUrls(Map.of("Some Url", UrlConfig.copyOfBuilder(exampleConfig().urls.values().stream().findFirst().get()).withUrl("https://www.notallowed.org").build()))
.build();

//When
jLineupRunnerFactory.sanitizeJobConfig(jobConfig);
}

@Test
public void shouldNotThrowIllegalArgumentExceptionWhenURLPrefixesAreNotSet() throws BrowserNotInstalledException {
// Given
JobConfig jobConfig = copyOfBuilder(exampleConfig())
.withUrls(Map.of("Some Url", UrlConfig.copyOfBuilder(exampleConfig().urls.values().stream().findFirst().get()).withUrl("https://www.notallowed.org").build()))
.build();

jLineupWebProperties.setAllowedUrlPrefixes(List.of());

//When
jLineupRunnerFactory.sanitizeJobConfig(jobConfig);
}

@Test
public void shouldUseMaxThreadsPerJobIfNoThreadsAreConfigured() throws BrowserNotInstalledException {
//Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ public void shouldFailIfBrowserIsNotConfigured() {
expectStatusCodeForConfig(jobConfig, UNPROCESSABLE_ENTITY.value());
}

@Test
public void shouldFailForForbiddenUrl() {
JobConfig jobConfig = copyOfBuilder(createTestConfig()).addUrlConfig("https://www.forbidden.com", UrlConfig.urlConfigBuilder().build()).build();
expectStatusCodeForConfig(jobConfig, UNPROCESSABLE_ENTITY.value());
}

private void assertReportExists(JLineupRunStatus finalState) {
when()
.get(contextPath + "/reports/report-" + finalState.getId() + "/report.json")
Expand Down
4 changes: 4 additions & 0 deletions web/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
jlineup:
installedBrowsers: chrome, chrome-headless
chromeLaunchParameters: --user-data-dir=/tmp/jlineup/chrome-profile-{id}, --force-device-scale-factor=1
allowedUrlPrefixes:
- https://www.otto.de
- https://develop.otto.de
- https://www.example.com

server:
servlet:
Expand Down

0 comments on commit 8819fe3

Please sign in to comment.