Skip to content

Commit 4ef2597

Browse files
committed
adding whitelist for freemarker in content templates
1 parent 6907d5d commit 4ef2597

File tree

6 files changed

+106
-45
lines changed

6 files changed

+106
-45
lines changed

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/Content.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.scmm.ScmmRepo
77
import com.cloudogu.gitops.scmm.ScmmRepoProvider
88
import com.cloudogu.gitops.scmm.api.ScmmApiClient
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
@@ -234,7 +235,8 @@ class Content extends Feature {
234235
engine.replaceTemplates(srcPath, [
235236
config : config,
236237
// Allow for using static classes inside the templates
237-
statics: new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels()
238+
statics: !config.content.useWhitelist ? new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() :
239+
new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, config.content.getAllowedStaticsWhitelist()).getStaticModels()
238240
])
239241
}
240242
}
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+
}

src/main/groovy/com/cloudogu/gitops/utils/AllowlistFreemarkerObjectWrapper.groovy

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/test/groovy/com/cloudogu/gitops/utils/AllowlistFreemarkerObjectWrapperTest.groovy

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,75 @@ import static org.junit.jupiter.api.Assertions.*
77

88
class AllowlistFreemarkerObjectWrapperTest {
99

10+
1011
@Test
1112
void 'should allow access to whitelisted static models'() {
12-
def wrapper = new AllowlistFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["java.lang.String"] as Set)
13+
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["com.cloudogu.gitops.utils.DockerImageParser"] as Set)
1314
def staticModels = wrapper.getStaticModels()
1415

15-
assertNotNull(staticModels.get("java.lang.String"))
16+
assertNotNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser"))
17+
assertNull(staticModels.get("java.lang.Integer"))
18+
assertNull(staticModels.get("java.lang.String"))
1619
}
1720

1821
@Test
1922
void 'should deny access to non-whitelisted static models'() {
20-
def wrapper = new AllowlistFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["java.lang.String"] as Set)
23+
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, ["java.lang.String"] as Set)
2124
def staticModels = wrapper.getStaticModels()
2225

2326
assertNull(staticModels.get("java.lang.Integer"))
27+
assertNotNull(staticModels.get("java.lang.String"))
28+
assertNull(staticModels.get("com.cloudogu.gitops.utils.DockerImageParser"))
2429
}
2530

2631
@Test
2732
void 'should return true for isEmpty when allowlist is empty'() {
28-
def wrapper = new AllowlistFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, [] as Set)
33+
def wrapper = new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, [] as Set)
2934
def staticModels = wrapper.getStaticModels()
3035

3136
assertTrue(staticModels.isEmpty())
3237
}
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+
}
3381
}

0 commit comments

Comments
 (0)