Skip to content

Commit 829cbd9

Browse files
Merge commit from fork
* Added security for email templates that used Jinja2 * Added security for email templates that used Jinja2 * Added security for email templates that used Jinja2 * QA to migration * Update CHANGELOG.md * Update src/fides/api/alembic/migrations/versions/ff81f8e7d6ae_migrate_messaging_template_to_use_new_.py * QA * Update migrations
1 parent e9174b7 commit 829cbd9

File tree

9 files changed

+182
-67
lines changed

9 files changed

+182
-67
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ The types of changes are:
3838
- TCF Optimized for performance on initial load by offsetting most experience data until after banner is shown [#5230](https://github.com/ethyca/fides/pull/5230)
3939
- Updates to support DynamoDB schema with Tokenless IAM auth [#5240](https://github.com/ethyca/fides/pull/5240)
4040

41+
### Security
42+
- Removed Jinja2 for email templates, the variables syntax changed from `{{variable_name}}` to `__VARIABLE_NAME__` [CVE-2024-45053](https://github.com/ethyca/fides/security/advisories/GHSA-c34r-238x-f7qx)
43+
4144
### Developer Experience
4245
- Sourcemaps are now working for fides-js in debug mode [#5222](https://github.com/ethyca/fides/pull/5222)
4346

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Changes the email template variables syntax from {{variable_name}} to __VARIABLE_NAME__.
2+
3+
Revision ID: eef4477c37d0
4+
Revises: cc37edf20859
5+
Create Date: 2024-09-03 12:47:54.708196
6+
7+
"""
8+
from alembic import op
9+
10+
11+
# revision identifiers, used by Alembic.
12+
revision = 'eef4477c37d0'
13+
down_revision = 'cc37edf20859'
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
VARIABLES = ["minutes", "days", "denial_reason", "code", "download_link"]
19+
JSON_VARIABLES = ["subject", "body"]
20+
21+
22+
def upgrade():
23+
for json_variable in JSON_VARIABLES:
24+
for variable in VARIABLES:
25+
statement = f"""
26+
UPDATE messaging_template
27+
SET content = jsonb_set(
28+
content,
29+
'{{{json_variable}}}',
30+
to_jsonb(REPLACE(content ->> '{json_variable}', '{{{{{variable}}}}}', '__{variable.upper()}__'))
31+
);
32+
"""
33+
op.execute(statement)
34+
35+
36+
def downgrade():
37+
for json_variable in JSON_VARIABLES:
38+
for variable in VARIABLES:
39+
statement = f"""
40+
UPDATE messaging_template
41+
SET content = jsonb_set(
42+
content,
43+
'{{{json_variable}}}',
44+
to_jsonb(REPLACE(content ->> '{json_variable}', '__{variable.upper()}__', '{{{{{variable}}}}}'))
45+
);
46+
"""
47+
op.execute(statement)

src/fides/api/models/messaging_template.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value: {
2020
"label": "Subject identity verification",
2121
"content": {
22-
"subject": "Your one-time code is {{code}}",
23-
"body": "Your privacy request verification code is {{code}}. Please return to the Privacy Center and enter the code to continue. This code will expire in {{minutes}} minutes.",
22+
"subject": "Your one-time code is __CODE__",
23+
"body": "Your privacy request verification code is __CODE__. Please return to the Privacy Center and enter the code to continue. This code will expire in __MINUTES__ minutes.",
2424
},
2525
},
2626
MessagingActionType.PRIVACY_REQUEST_RECEIPT.value: {
@@ -41,14 +41,14 @@
4141
"label": "Privacy request denied",
4242
"content": {
4343
"subject": "Your privacy request has been denied",
44-
"body": "Your privacy request has been denied. {{denial_reason}}.",
44+
"body": "Your privacy request has been denied. __DENIAL_REASON__.",
4545
},
4646
},
4747
MessagingActionType.PRIVACY_REQUEST_COMPLETE_ACCESS.value: {
4848
"label": "Access request completed",
4949
"content": {
5050
"subject": "Your data is ready to be downloaded",
51-
"body": "Your access request has been completed and can be downloaded at {{download_link}}. For security purposes, this secret link will expire in {{days}} days.",
51+
"body": "Your access request has been completed and can be downloaded at __DOWNLOAD_LINK__. For security purposes, this secret link will expire in __DAYS__ days.",
5252
},
5353
},
5454
MessagingActionType.PRIVACY_REQUEST_COMPLETE_DELETION.value: {

src/fides/api/service/messaging/message_dispatch_service.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import requests
77
import sendgrid
8-
from jinja2 import Environment
98
from loguru import logger
109
from sendgrid.helpers.mail import Content, Email, Mail, Personalization, TemplateId, To
1110
from sqlalchemy.orm import Session
@@ -384,9 +383,11 @@ def _render(template_str: str, variables: Optional[Dict] = None) -> str:
384383
"""Helper function to render a template string with the provided variables."""
385384
if variables is None:
386385
variables = {}
387-
jinja_env = Environment()
388-
template = jinja_env.from_string(template_str)
389-
return template.render(variables)
386+
387+
for key, value in variables.items():
388+
template_str = template_str.replace(f"__{key.upper()}__", str(value))
389+
390+
return template_str
390391

391392

392393
def _build_email( # pylint: disable=too-many-return-statements

tests/fixtures/application_fixtures.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,8 @@ def property_b(db: Session) -> Generator:
401401
def messaging_template_with_property_disabled(db: Session, property_a) -> Generator:
402402
template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value
403403
content = {
404-
"subject": "Here is your code {{code}}",
405-
"body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!",
404+
"subject": "Here is your code __CODE__",
405+
"body": "Use code __CODE__ to verify your identity, you have __MINUTES__ minutes!",
406406
}
407407
data = {
408408
"content": content,
@@ -422,8 +422,8 @@ def messaging_template_with_property_disabled(db: Session, property_a) -> Genera
422422
def messaging_template_no_property_disabled(db: Session) -> Generator:
423423
template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value
424424
content = {
425-
"subject": "Here is your code {{code}}",
426-
"body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!",
425+
"subject": "Here is your code __CODE__",
426+
"body": "Use code __CODE__ to verify your identity, you have __MINUTES__ minutes!",
427427
}
428428
data = {
429429
"content": content,
@@ -443,8 +443,8 @@ def messaging_template_no_property_disabled(db: Session) -> Generator:
443443
def messaging_template_no_property(db: Session) -> Generator:
444444
template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value
445445
content = {
446-
"subject": "Here is your code {{code}}",
447-
"body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!",
446+
"subject": "Here is your code __CODE__",
447+
"body": "Use code __CODE__ to verify your identity, you have __MINUTES__ minutes!",
448448
}
449449
data = {
450450
"content": content,
@@ -466,8 +466,8 @@ def messaging_template_subject_identity_verification(
466466
) -> Generator:
467467
template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value
468468
content = {
469-
"subject": "Here is your code {{code}}",
470-
"body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!",
469+
"subject": "Here is your code __CODE__",
470+
"body": "Use code __CODE__ to verify your identity, you have __MINUTES__ minutes!",
471471
}
472472
data = {
473473
"content": content,

tests/ops/api/v1/endpoints/test_messaging_endpoints.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,8 +1944,8 @@ def payload(self) -> List[Dict[str, Any]]:
19441944
{
19451945
"type": "subject_identity_verification",
19461946
"content": {
1947-
"body": "Your privacy request verification code is {{code}}. Please return to the Privacy Center and enter the code to continue. You have {{minutes}} minutes.",
1948-
"subject": "Your code is {{code}}",
1947+
"body": "Your privacy request verification code is __CODE__. Please return to the Privacy Center and enter the code to continue. You have __MINUTES__ minutes.",
1948+
"subject": "Your code is __CODE__",
19491949
},
19501950
},
19511951
]
@@ -1979,8 +1979,8 @@ def test_put_messaging_templates(
19791979
{
19801980
"type": "subject_identity_verification",
19811981
"content": {
1982-
"body": "Your privacy request verification code is {{code}}. Please return to the Privacy Center and enter the code to continue. You have {{minutes}} minutes.",
1983-
"subject": "Your code is {{code}}",
1982+
"body": "Your privacy request verification code is __CODE__. Please return to the Privacy Center and enter the code to continue. You have __MINUTES__ minutes.",
1983+
"subject": "Your code is __CODE__",
19841984
},
19851985
"label": "Subject identity verification",
19861986
}
@@ -2015,8 +2015,8 @@ def test_put_messaging_templates_missing_values(
20152015
{
20162016
"type": "subject_identity_verification",
20172017
"content": {
2018-
"body": "Your privacy request verification code is {{code}}. Please return to the Privacy Center and enter the code to continue. This code will expire in {{minutes}} minutes.",
2019-
"subject": "Your one-time code is {{code}}",
2018+
"body": "Your privacy request verification code is __CODE__. Please return to the Privacy Center and enter the code to continue. This code will expire in __MINUTES__ minutes.",
2019+
"subject": "Your one-time code is __CODE__",
20202020
},
20212021
"label": "Subject identity verification",
20222022
}
@@ -2202,8 +2202,8 @@ def test_delete_messaging_template_by_id_success(
22022202
# Creating new config, so we don't run into issues trying to clean up a deleted fixture
22032203
template_type = MessagingActionType.SUBJECT_IDENTITY_VERIFICATION.value
22042204
content = {
2205-
"subject": "Here is your code {{code}}",
2206-
"body": "Use code {{code}} to verify your identity, you have {{minutes}} minutes!",
2205+
"subject": "Here is your code __CODE__",
2206+
"body": "Use code __CODE__ to verify your identity, you have __MINUTES__ minutes!",
22072207
}
22082208
data = {
22092209
"content": content,

tests/ops/service/messaging/message_dispatch_service_test.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ def test_email_dispatch_mailgun_success(
109109
Test scenario:
110110
✅︎ Property-specific messaging is enabled
111111
❌ No template configured for action type
112-
113-
Result: Email is not sent. An explicit messaging template with matching action type is needed to send emails for
112+
113+
Result: Email is not sent. An explicit messaging template with matching action type is needed to send emails for
114114
property-specific messaging
115115
"""
116116

@@ -140,7 +140,7 @@ def test_email_dispatch_property_specific_templates_enabled_no_template(
140140
Test scenario:
141141
❌ Property-specific messaging is disabled
142142
✅︎ Has template configured for action type
143-
143+
144144
Result: Email sent the template configured with matching action type.
145145
"""
146146

@@ -178,7 +178,7 @@ def test_email_dispatch_property_specific_templates_disabled_with_template(
178178
Test scenario:
179179
❌ Property-specific messaging is disabled
180180
❌ No template configured for action type
181-
181+
182182
Result: Email sent with default messaging template.
183183
"""
184184

@@ -217,7 +217,7 @@ def test_email_dispatch_property_specific_templates_disabled_no_template(
217217
✅︎ Has template configured for action type
218218
❌ No property id attached to template
219219
❌ No property id in request
220-
220+
221221
Result: Email not sent. There was no explicit property id linked to the template with matching action type.
222222
"""
223223

@@ -250,7 +250,7 @@ def test_email_dispatch_property_specific_templates_enabled_with_template_no_pro
250250
✅︎ Has template configured for action type
251251
✅︎ Default property id attached to template
252252
❌ No property id in request
253-
253+
254254
Result: Email sent using template linked to default property id. If no property id was received, we assume
255255
the default property id to look up the associated messaging template.
256256
"""
@@ -294,7 +294,7 @@ def test_email_dispatch_property_specific_templates_enabled_with_template_has_pr
294294
✅︎ Has template configured for action type
295295
❌ No property attached to template
296296
✅ Default property id in request
297-
297+
298298
Result: Email not sent. There was no explicit property id linked to the template with matching action type.
299299
"""
300300

@@ -328,7 +328,7 @@ def test_email_dispatch_property_specific_templates_enabled_with_template_no_pro
328328
✅︎ Has template configured for action type
329329
✅ Property attached to template
330330
✅ Matching property id in request
331-
331+
332332
Result: Email sent using template with with property id
333333
"""
334334

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import pytest
2+
3+
from fides.api.service.messaging.message_dispatch_service import _render
4+
5+
6+
@pytest.mark.unit
7+
class TestMessageTemplateRender:
8+
9+
def test_template_render(self):
10+
"""
11+
Test that a template is rendered correctly with the given variables.
12+
"""
13+
template_str = """
14+
Your privacy request has been denied.
15+
__DENIAL_REASON__
16+
"""
17+
variables = {
18+
"denial_reason": "Accounts with an unpaid balance cannot be deleted."
19+
}
20+
21+
expected_rendered_template = """
22+
Your privacy request has been denied.
23+
Accounts with an unpaid balance cannot be deleted.
24+
"""
25+
26+
rendered_template = _render(template_str, variables)
27+
assert rendered_template == expected_rendered_template
28+
29+
def test_template_render_unsafe(self):
30+
"""
31+
Test that a template with unsafe code is not rendered and raises a SecurityError.
32+
"""
33+
template_str = """
34+
Your privacy request has been denied.
35+
*bb*
36+
{% for s in ().__class__.__base__.__subclasses__() %}{% if "warning" in s.__name__ %}{{s()._module.__builtins__['__import__']('os').popen("env").read() }}{% endif %}
37+
{% endfor %}
38+
__CONFIG__
39+
*aa*
40+
"""
41+
42+
expected_rendered_template = """
43+
Your privacy request has been denied.
44+
*bb*
45+
{% for s in ().__class__.__base__.__subclasses__() %}{% if "warning" in s.__name__ %}{{s()._module.__builtins__['__import__']('os').popen("env").read() }}{% endif %}
46+
{% endfor %}
47+
123
48+
*aa*
49+
"""
50+
51+
variables = {
52+
"config": "123",
53+
}
54+
55+
rendered_template = _render(template_str, variables)
56+
assert rendered_template == expected_rendered_template
57+
58+
template_str = "your privacy request has been denied. __CONFIG.security.app_encryption_key__"
59+
variables = {
60+
"config": "123",
61+
}
62+
63+
rendered_template = _render(template_str, variables)
64+
assert rendered_template == template_str

0 commit comments

Comments
 (0)