Skip to content

Commit d60b87e

Browse files
authored
adding content whitelist for freemarker statics templating (#316)
* adding a AllowListforFreemarker * docs added * adding whitelist for freemarker in content templates
1 parent ae2015b commit d60b87e

File tree

7 files changed

+149
-4
lines changed

7 files changed

+149
-4
lines changed

docs/configuration.schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@
155155
"content" : {
156156
"type" : [ "object", "null" ],
157157
"properties" : {
158+
"allowedStaticsWhitelist" : {
159+
"description" : "Whitelist for Statics freemarker is allowing in user templates",
160+
"type" : [ "array", "null" ],
161+
"items" : {
162+
"type" : "string"
163+
}
164+
},
158165
"examples" : {
159166
"type" : [ "boolean", "null" ],
160167
"description" : "Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project"
@@ -230,6 +237,10 @@
230237
"additionalProperties" : false
231238
}
232239
},
240+
"useWhitelist" : {
241+
"type" : [ "boolean", "null" ],
242+
"description" : "Enables the whitelist for statics in content templating"
243+
},
233244
"variables" : {
234245
"$ref" : "#/$defs/Map(String,Object)-nullable",
235246
"description" : "Additional variables to use in custom templates."

docs/content-loader/content-loader.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ image:
252252
tag : ${operatorImageObject.tag}
253253
</#if>
254254
```
255-
255+
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.
256+
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.
256257
# TL;DR
257258

258259
How to get started with content loader?
@@ -323,5 +324,4 @@ Reminder: no type means `MIRROR` (default).
323324
templating: true
324325
type: FOLDER_BASED
325326
overrideMode: UPGRADE
326-
```
327-
327+
```

src/main/groovy/com/cloudogu/gitops/config/Config.groovy

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ class Config {
104104
@JsonPropertyDescription(CONTENT_VARIABLES_DESCRIPTION)
105105
Map<String, Object> variables = [:]
106106

107+
@Option(names = ['--content-whitelist'], description = CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
108+
@JsonPropertyDescription(CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
109+
Boolean useWhitelist = false
110+
111+
@JsonPropertyDescription(CONTENT_STATICSWHITELIST_DESCRIPTION)
112+
Set<String> allowedStaticsWhitelist = [
113+
'java.lang.String',
114+
'java.lang.Integer',
115+
'java.lang.Long',
116+
'java.lang.Double',
117+
'java.lang.Float',
118+
'java.lang.Boolean',
119+
'java.lang.Math',
120+
'com.cloudogu.gitops.utils.DockerImageParser'
121+
] as Set<String>
122+
107123
static class ContentRepositorySchema {
108124
static final String DEFAULT_PATH = '.'
109125
// This is controversial. Forcing users to explicitly choose a type requires them to understand the concept

src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ interface ConfigConstants {
4343
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."
4444
String CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION = "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches."
4545
String CONTENT_VARIABLES_DESCRIPTION = "Additional variables to use in custom templates."
46+
String CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION = 'Enables the whitelist for statics in content templating'
47+
String CONTENT_STATICSWHITELIST_DESCRIPTION = 'Whitelist for Statics freemarker is allowing in user templates'
4648

4749
// group jenkins
4850
String JENKINS_ENABLE_DESCRIPTION = 'Installs Jenkins as CI server'

src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.cloudogu.gitops.config.Config.OverwriteMode
66
import com.cloudogu.gitops.features.git.GitHandler
77
import com.cloudogu.gitops.git.GitRepo
88
import com.cloudogu.gitops.git.GitRepoFactory
9+
import com.cloudogu.gitops.utils.AllowListFreemarkerObjectWrapper
910
import com.cloudogu.gitops.utils.FileSystemUtils
1011
import com.cloudogu.gitops.utils.K8sClient
1112
import com.cloudogu.gitops.utils.TemplatingEngine
@@ -229,7 +230,8 @@ class ContentLoader extends Feature {
229230
engine.replaceTemplates(srcPath, [
230231
config : config,
231232
// Allow for using static classes inside the templates
232-
statics: new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels()
233+
statics: !config.content.useWhitelist ? new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() :
234+
new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, config.content.getAllowedStaticsWhitelist()).getStaticModels()
233235
])
234236
}
235237
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.cloudogu.gitops.utils
2+
3+
import freemarker.template.*
4+
5+
class AllowListFreemarkerObjectWrapper extends DefaultObjectWrapper {
6+
7+
Set<String> allowlist
8+
9+
AllowListFreemarkerObjectWrapper(Version freemarkerVersion, Set<String> allowlist) {
10+
super(freemarkerVersion)
11+
this.allowlist = allowlist
12+
}
13+
14+
TemplateHashModel getStaticModels() {
15+
final TemplateHashModel originalStaticModels = super.getStaticModels()
16+
final Set<String> allowlistCopy = this.allowlist
17+
18+
return new TemplateHashModel() {
19+
@Override
20+
TemplateModel get(String key) throws TemplateModelException {
21+
if (allowlistCopy.contains(key)) {
22+
return originalStaticModels.get(key)
23+
}
24+
return null
25+
}
26+
27+
@Override
28+
boolean isEmpty() throws TemplateModelException {
29+
return allowlistCopy.isEmpty()
30+
}
31+
}
32+
}
33+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.cloudogu.gitops.utils
2+
3+
import freemarker.template.Configuration
4+
import org.junit.jupiter.api.Test
5+
6+
import static org.junit.jupiter.api.Assertions.*
7+
8+
class AllowlistFreemarkerObjectWrapperTest {
9+
10+
11+
@Test
12+
void 'should allow access to whitelisted static models'() {
13+
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["com.cloudogu.gitops.utils.DockerImageParser"] as Set)
14+
def staticModels = wrapper.getStaticModels()
15+
16+
assertNotNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser"))
17+
assertNull(staticModels.get("java.lang.Integer"))
18+
assertNull(staticModels.get("java.lang.String"))
19+
}
20+
21+
@Test
22+
void 'should deny access to non-whitelisted static models'() {
23+
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["java.lang.String"] as Set)
24+
def staticModels = wrapper.getStaticModels()
25+
26+
assertNull(staticModels.get("java.lang.Integer"))
27+
assertNotNull(staticModels.get("java.lang.String"))
28+
assertNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser"))
29+
}
30+
31+
@Test
32+
void 'should return true for isEmpty when allowlist is empty'() {
33+
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, [] as Set)
34+
def staticModels = wrapper.getStaticModels()
35+
36+
assertTrue(staticModels.isEmpty())
37+
}
38+
39+
@Test
40+
void 'templating only works for whitelisted statics'() {
41+
def templateText = '''
42+
<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']>
43+
<#assign imageObject = DockerImageParser.parse('test:latest')>
44+
<#assign staticsTests=statics['System']>
45+
<#assign imageObject = staticsTests.exit()>
46+
'''.stripIndent()
47+
48+
def model = [
49+
statics: new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ['com.cloudogu.gitops.utils.DockerImageParser'] as Set).getStaticModels()
50+
] as Map<String, Object>
51+
// create a temporary file to simulate an actual file input
52+
def tempInputFile = File.createTempFile("test", ".ftl.yaml")
53+
tempInputFile.text = templateText
54+
55+
def exception = assertThrows(freemarker.core.InvalidReferenceException) {
56+
new TemplatingEngine().replaceTemplates(tempInputFile, model)
57+
}
58+
59+
assert exception.message.contains("System") : "Exception message should mention 'System'"
60+
}
61+
62+
@Test
63+
void 'templating in ftl files works correctly with whitelisted static models'() {
64+
def templateText = '''
65+
<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']>
66+
<#assign imageObject = DockerImageParser.parse('test:latest')>
67+
<#assign staticsTests=statics['java.lang.Math']>
68+
<#assign number = staticsTests.round(3.14)>
69+
'''.stripIndent()
70+
71+
def model = [
72+
statics: new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ['java.lang.Math', 'com.cloudogu.gitops.utils.DockerImageParser'] as Set).getStaticModels()
73+
] as Map<String, Object>
74+
// create a temporary file to simulate an actual file input
75+
def tempInputFile = File.createTempFile("test", ".ftl.yaml")
76+
tempInputFile.text = templateText
77+
78+
new TemplatingEngine().replaceTemplates(tempInputFile, model)
79+
80+
}
81+
}

0 commit comments

Comments
 (0)