Skip to content
Permalink
Browse files Browse the repository at this point in the history
Merge pull request from GHSA-3jmw-c69h-426c
Add CSRF protection to plugin upload/install endpoints
  • Loading branch information
gschueler committed Aug 13, 2021
2 parents 850d12e + e6acaf0 commit 67c4eed
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 30 deletions.
Expand Up @@ -13,24 +13,31 @@ import com.dtolabs.rundeck.core.plugins.configuration.PluginAdapterUtility
import grails.converters.JSON
import groovy.transform.CompileStatic
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.multipart.MultipartHttpServletRequest
import org.springframework.web.servlet.support.RequestContextUtils
import rundeck.services.ApiService
import rundeck.services.FrameworkService
import rundeck.services.PluginApiService
import rundeck.services.PluginService
import rundeck.services.UiPluginService
import rundeck.services.feature.FeatureService

import javax.servlet.http.HttpServletResponse
import java.text.SimpleDateFormat

import static org.springframework.http.HttpStatus.NOT_FOUND

class PluginController extends ControllerBase {
private static final String RELATIVE_PLUGIN_UPLOAD_DIR = "var/tmp/pluginUpload"
private static final SimpleDateFormat PLUGIN_DATE_FMT = new SimpleDateFormat("EEE MMM dd hh:mm:ss Z yyyy")
static def allowedMethods = [
installPlugin: ['POST'],
uploadPlugin: ['POST']
]
UiPluginService uiPluginService
PluginService pluginService
PluginApiService pluginApiService
FrameworkService frameworkService
ApiService apiService
def featureService
AppAuthContextProcessor rundeckAuthContextProcessor
AuthorizedServicesProvider rundeckAuthorizedServicesProvider
Expand Down Expand Up @@ -422,13 +429,31 @@ class PluginController extends ControllerBase {
stream.close()
}
}
protected boolean requireAjaxFormToken(){
boolean valid = false
withForm {
g.refreshFormTokensHeader()
valid = true
}.invalidToken {
}
if (!valid) {
apiService.renderErrorFormat(response, [
status: HttpServletResponse.SC_BAD_REQUEST,
code: 'request.error.invalidtoken.message',
])
}
return valid
}

def uploadPlugin() {

if(featureService.featurePresent(Features.PLUGIN_SECURITY)){
renderErrorCodeAsJson("plugin.error.unauthorized.upload")
return
}
if(!requireAjaxFormToken()){
return
}

AuthContext authContext = rundeckAuthContextProcessor.getAuthContextForSubject(session.subject)
boolean authorized = rundeckAuthContextProcessor.authorizeApplicationResourceType(authContext,
Expand All @@ -438,15 +463,16 @@ class PluginController extends ControllerBase {
renderErrorCodeAsJson("request.error.unauthorized.title")
return
}
if(!params.pluginFile || params.pluginFile.isEmpty()) {
if(!(request instanceof MultipartHttpServletRequest && request.getFile('pluginFile'))){
renderErrorCodeAsJson("plugin.error.missing.upload.file")
return
}
def file = request.getFile('pluginFile')
ensureUploadLocation()
File tmpFile = new File(frameworkService.getRundeckFramework().baseDir,RELATIVE_PLUGIN_UPLOAD_DIR+"/"+params.pluginFile.originalFilename)
File tmpFile = new File(frameworkService.getRundeckFramework().baseDir,RELATIVE_PLUGIN_UPLOAD_DIR+"/"+file.originalFilename)
if(tmpFile.exists()) tmpFile.delete()
tmpFile << ((MultipartFile)params.pluginFile).inputStream
def errors = validateAndCopyPlugin(params.pluginFile.originalFilename, tmpFile)
tmpFile << file.inputStream
def errors = validateAndCopyPlugin(file.originalFilename, tmpFile)
tmpFile.delete()
def msg = [:]
if(!errors.isEmpty()) {
Expand All @@ -459,6 +485,9 @@ class PluginController extends ControllerBase {
}

def installPlugin() {
if(!requireAjaxFormToken()){
return
}
AuthContext authContext = rundeckAuthContextProcessor.getAuthContextForSubject(session.subject)
boolean authorized = rundeckAuthContextProcessor.authorizeApplicationResourceType(authContext,
"system",
Expand Down
Expand Up @@ -18,7 +18,7 @@
</div>
</template>
<script>
import axios from "axios";
import {client} from "@rundeck/ui-trellis/lib/modules/rundeckClient"
export default {
name: "PluginUrlUploadForm",
data() {
Expand All @@ -32,26 +32,38 @@ export default {
loadingMessage: "Installing",
loadingSpinner: true
});
axios({
method: "post",
headers: {
"x-rundeck-ajax": true
client.sendRequest({
baseUrl: window._rundeck.rdBase,
pathTemplate: `/plugin/installPlugin`,
queryParameters: {
pluginUrl: this.pluginURL
},
url: `${window._rundeck.rdBase}plugin/installPlugin?pluginUrl=${
this.pluginURL
}`,
withCredentials: true
method: 'POST'
}).then(response => {
this.$store.dispatch("overlay/openOverlay");
if (response.data.err) {
if (response.status === 200) {
this.$store.dispatch("overlay/openOverlay");
if (response.parsedBody.err) {
this.$alert({
title: "Error Uploading",
content: response.parsedBody.err
});
} else {
this.$alert({
title: "Plugin Installed",
content: response.parsedBody.msg
});
}
}else if (response.status >= 300) {
this.$store.dispatch("overlay/openOverlay");
let message = `Error: ${response.status}`
if (response.parsedBody && response.parsedBody.message) {
message = response.parsedBody.message
}else if (response.parsedBody && response.parsedBody.error) {
message = response.parsedBody.error
}
this.$alert({
title: "Error Uploading",
content: response.data.err
});
} else {
this.$alert({
title: "Plugin Installed",
content: response.data.msg
content: message
});
}
});
Expand Down
Expand Up @@ -27,6 +27,7 @@
<script>
import { mapState, mapActions } from "vuex";
import axios from "axios";
import {client} from "@rundeck/ui-trellis/lib/modules/rundeckClient"
export default {
name: "UploadPluginForm",
computed: {
Expand All @@ -52,17 +53,22 @@ export default {
loadingMessage: "Installing",
loadingSpinner: true
});
//use axios instead of RundeckClient, to allow multipart form with file upload
axios({
method: "post",
headers: {
"x-rundeck-ajax": true,
"Content-Type": "multipart/form-data"
"Content-Type": "multipart/form-data",
"X-RUNDECK-TOKEN-KEY": client.token,
"X-RUNDECK-TOKEN-URI": client.uri
},
data: formData,
url: `${window._rundeck.rdBase}plugin/uploadPlugin`,
withCredentials: true
}).then(response => {
this.$store.dispatch("overlay/openOverlay");
client.token = response.headers['x-rundeck-token-key'] || client.token
client.uri = response.headers['x-rundeck-token-uri'] || client.uri
if (response.data.err) {
this.$alert({
title: "Error Uploading",
Expand All @@ -74,6 +80,16 @@ export default {
content: response.data.msg
});
}
}).catch(result=>{
this.$store.dispatch("overlay/openOverlay");
let message=result.message
if(result.response && result.response.data && result.response.data.message){
message=result.response.data.message
}
this.$alert({
title: "Error Uploading",
content: message
});
});
},
handleFilesUploads() {
Expand Down

0 comments on commit 67c4eed

Please sign in to comment.