diff --git a/docs/configuration.schema.json b/docs/configuration.schema.json index 0f6147181..e0305e5c8 100644 --- a/docs/configuration.schema.json +++ b/docs/configuration.schema.json @@ -155,6 +155,13 @@ "content" : { "type" : [ "object", "null" ], "properties" : { + "allowedStaticsWhitelist" : { + "description" : "Whitelist for Statics freemarker is allowing in user templates", + "type" : [ "array", "null" ], + "items" : { + "type" : "string" + } + }, "examples" : { "type" : [ "boolean", "null" ], "description" : "Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project" @@ -230,6 +237,10 @@ "additionalProperties" : false } }, + "useWhitelist" : { + "type" : [ "boolean", "null" ], + "description" : "Enables the whitelist for statics in content templating" + }, "variables" : { "$ref" : "#/$defs/Map(String,Object)-nullable", "description" : "Additional variables to use in custom templates." diff --git a/docs/content-loader/content-loader.md b/docs/content-loader/content-loader.md index c49e192fe..0d0442059 100644 --- a/docs/content-loader/content-loader.md +++ b/docs/content-loader/content-loader.md @@ -252,7 +252,8 @@ image: tag : ${operatorImageObject.tag} ``` - +By default, Freemarker grants access to all static resources within the project. To add an extra layer of security, set the content.useWhitelist property to true in the GOP configuration, or use the --content-whitelist CLI flag to enable the static resources whitelist. +To specify which static resources should be accessible, add them to the allowedStaticsWhitelist in the configuration. A default set of static resources is already provided as an example. # TL;DR How to get started with content loader? @@ -323,5 +324,4 @@ Reminder: no type means `MIRROR` (default). templating: true type: FOLDER_BASED overrideMode: UPGRADE -``` - +``` \ No newline at end of file diff --git a/src/main/groovy/com/cloudogu/gitops/config/Config.groovy b/src/main/groovy/com/cloudogu/gitops/config/Config.groovy index 22a8ebfc7..6c32e4324 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/Config.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/Config.groovy @@ -104,6 +104,22 @@ class Config { @JsonPropertyDescription(CONTENT_VARIABLES_DESCRIPTION) Map variables = [:] + @Option(names = ['--content-whitelist'], description = CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION) + @JsonPropertyDescription(CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION) + Boolean useWhitelist = false + + @JsonPropertyDescription(CONTENT_STATICSWHITELIST_DESCRIPTION) + Set allowedStaticsWhitelist = [ + 'java.lang.String', + 'java.lang.Integer', + 'java.lang.Long', + 'java.lang.Double', + 'java.lang.Float', + 'java.lang.Boolean', + 'java.lang.Math', + 'com.cloudogu.gitops.utils.DockerImageParser' + ] as Set + static class ContentRepositorySchema { static final String DEFAULT_PATH = '.' // This is controversial. Forcing users to explicitly choose a type requires them to understand the concept diff --git a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy index 05e6777fc..0037b28dd 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy @@ -43,6 +43,8 @@ interface ConfigConstants { String CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION = "This defines, how customer repos will be updated.\nINIT - push only if repo does not exist.\nRESET - delete all files after cloning source - files not in content are deleted\nUPGRADE - clone and copy - existing files will be overwritten, files not in content are kept. For type: MIRROR reset and upgrade have same result: in both cases source repo will be force pushed to target repo." String CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION = "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches." String CONTENT_VARIABLES_DESCRIPTION = "Additional variables to use in custom templates." + String CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION = 'Enables the whitelist for statics in content templating' + String CONTENT_STATICSWHITELIST_DESCRIPTION = 'Whitelist for Statics freemarker is allowing in user templates' // group jenkins String JENKINS_ENABLE_DESCRIPTION = 'Installs Jenkins as CI server' diff --git a/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy b/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy index 93ab21b12..e8ce0747e 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy @@ -6,6 +6,7 @@ import com.cloudogu.gitops.config.Config.OverwriteMode import com.cloudogu.gitops.features.git.GitHandler import com.cloudogu.gitops.git.GitRepo import com.cloudogu.gitops.git.GitRepoFactory +import com.cloudogu.gitops.utils.AllowListFreemarkerObjectWrapper import com.cloudogu.gitops.utils.FileSystemUtils import com.cloudogu.gitops.utils.K8sClient import com.cloudogu.gitops.utils.TemplatingEngine @@ -229,7 +230,8 @@ class ContentLoader extends Feature { engine.replaceTemplates(srcPath, [ config : config, // Allow for using static classes inside the templates - statics: new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() + statics: !config.content.useWhitelist ? new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() : + new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, config.content.getAllowedStaticsWhitelist()).getStaticModels() ]) } } diff --git a/src/main/groovy/com/cloudogu/gitops/utils/AllowListFreemarkerObjectWrapper.groovy b/src/main/groovy/com/cloudogu/gitops/utils/AllowListFreemarkerObjectWrapper.groovy new file mode 100644 index 000000000..4ed0d5fcf --- /dev/null +++ b/src/main/groovy/com/cloudogu/gitops/utils/AllowListFreemarkerObjectWrapper.groovy @@ -0,0 +1,33 @@ +package com.cloudogu.gitops.utils + +import freemarker.template.* + +class AllowListFreemarkerObjectWrapper extends DefaultObjectWrapper { + + Set allowlist + + AllowListFreemarkerObjectWrapper(Version freemarkerVersion, Set allowlist) { + super(freemarkerVersion) + this.allowlist = allowlist + } + + TemplateHashModel getStaticModels() { + final TemplateHashModel originalStaticModels = super.getStaticModels() + final Set allowlistCopy = this.allowlist + + return new TemplateHashModel() { + @Override + TemplateModel get(String key) throws TemplateModelException { + if (allowlistCopy.contains(key)) { + return originalStaticModels.get(key) + } + return null + } + + @Override + boolean isEmpty() throws TemplateModelException { + return allowlistCopy.isEmpty() + } + } + } +} \ No newline at end of file diff --git a/src/test/groovy/com/cloudogu/gitops/utils/AllowlistFreemarkerObjectWrapperTest.groovy b/src/test/groovy/com/cloudogu/gitops/utils/AllowlistFreemarkerObjectWrapperTest.groovy new file mode 100644 index 000000000..112647ac5 --- /dev/null +++ b/src/test/groovy/com/cloudogu/gitops/utils/AllowlistFreemarkerObjectWrapperTest.groovy @@ -0,0 +1,81 @@ +package com.cloudogu.gitops.utils + +import freemarker.template.Configuration +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.* + +class AllowlistFreemarkerObjectWrapperTest { + + + @Test + void 'should allow access to whitelisted static models'() { + def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["com.cloudogu.gitops.utils.DockerImageParser"] as Set) + def staticModels = wrapper.getStaticModels() + + assertNotNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser")) + assertNull(staticModels.get("java.lang.Integer")) + assertNull(staticModels.get("java.lang.String")) + } + + @Test + void 'should deny access to non-whitelisted static models'() { + def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["java.lang.String"] as Set) + def staticModels = wrapper.getStaticModels() + + assertNull(staticModels.get("java.lang.Integer")) + assertNotNull(staticModels.get("java.lang.String")) + assertNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser")) + } + + @Test + void 'should return true for isEmpty when allowlist is empty'() { + def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, [] as Set) + def staticModels = wrapper.getStaticModels() + + assertTrue(staticModels.isEmpty()) + } + + @Test + void 'templating only works for whitelisted statics'() { + def templateText = ''' + <#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']> + <#assign imageObject = DockerImageParser.parse('test:latest')> + <#assign staticsTests=statics['System']> + <#assign imageObject = staticsTests.exit()> + '''.stripIndent() + + def model = [ + statics: new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ['com.cloudogu.gitops.utils.DockerImageParser'] as Set).getStaticModels() + ] as Map + // create a temporary file to simulate an actual file input + def tempInputFile = File.createTempFile("test", ".ftl.yaml") + tempInputFile.text = templateText + + def exception = assertThrows(freemarker.core.InvalidReferenceException) { + new TemplatingEngine().replaceTemplates(tempInputFile, model) + } + + assert exception.message.contains("System") : "Exception message should mention 'System'" + } + + @Test + void 'templating in ftl files works correctly with whitelisted static models'() { + def templateText = ''' +<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']> +<#assign imageObject = DockerImageParser.parse('test:latest')> +<#assign staticsTests=statics['java.lang.Math']> +<#assign number = staticsTests.round(3.14)> + '''.stripIndent() + + def model = [ + statics: new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ['java.lang.Math', 'com.cloudogu.gitops.utils.DockerImageParser'] as Set).getStaticModels() + ] as Map + // create a temporary file to simulate an actual file input + def tempInputFile = File.createTempFile("test", ".ftl.yaml") + tempInputFile.text = templateText + + new TemplatingEngine().replaceTemplates(tempInputFile, model) + + } +} \ No newline at end of file