Skip to content

Commit f2c90aa

Browse files
authored
[pyyaml] allow patching of unsafe pyyaml operations (#3808)
[utils] changing yaml to ddyaml to avoid clash [pyyaml] allow reversing of monkey patch
1 parent 59ab0a8 commit f2c90aa

File tree

10 files changed

+219
-13
lines changed

10 files changed

+219
-13
lines changed

.rubocop.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ Style/Documentation:
2424
Style/SingleLineBlockParams:
2525
Enabled: false
2626

27+
Style/FrozenStringLiteralComment:
28+
Enabled: false
29+
2730
Lint/RescueWithoutErrorClass:
2831
Enabled: false
2932

3033
BlockLength:
3134
Max: 110
3235

33-
Style/EndOfLine:
36+
Layout/EndOfLine:
3437
Enabled: false
35-

agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from utils.service_discovery.sd_backend import get_sd_backend
6464
from utils.watchdog import Watchdog
6565
from utils.windows_configuration import get_sdk_integration_paths
66+
from utils.ddyaml import monkey_patch_pyyaml
6667

6768
# Constants
6869
PID_NAME = "dd-agent"
@@ -502,6 +503,10 @@ def main():
502503
hostname = get_hostname(agentConfig)
503504
in_developer_mode = agentConfig.get('developer_mode')
504505

506+
# do this early on
507+
if agentConfig.get('disable_unsafe_yaml'):
508+
monkey_patch_pyyaml()
509+
505510
COMMANDS_AGENT = [
506511
'start',
507512
'stop',

checks/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@
3030
# project
3131
from checks import check_status
3232
from config import AGENT_VERSION, _is_affirmative
33-
from util import get_next_id, yLoader
33+
from util import get_next_id
3434
from utils.hostname import get_hostname
3535
from utils.proxy import get_proxy
3636
from utils.profile import pretty_statistics
3737
from utils.proxy import get_no_proxy_from_env, config_proxy_skip
38+
from utils.ddyaml import yLoader
3839

3940

4041
log = logging.getLogger(__name__)

config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,10 @@ def get_config(parse_args=True, cfg_path=None, options=None, can_query_registry=
601601
if config.has_option("Main", "skip_ssl_validation"):
602602
agentConfig["skip_ssl_validation"] = _is_affirmative(config.get("Main", "skip_ssl_validation"))
603603

604+
agentConfig["disable_unsafe_yaml"] = True
605+
if config.has_option("Main", "disable_unsafe_yaml"):
606+
agentConfig["disable_unsafe_yaml"] = _is_affirmative(config.get("Main", "disable_unsafe_yaml"))
607+
604608
agentConfig["collect_instance_metadata"] = True
605609
if config.has_option("Main", "collect_instance_metadata"):
606610
agentConfig["collect_instance_metadata"] = _is_affirmative(config.get("Main", "collect_instance_metadata"))

jmxfetch.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
_is_affirmative,
2929
)
3030
from daemon import ProcessRunner
31-
from util import yLoader
3231
from utils.jmx import JMX_FETCH_JAR_NAME, JMXFiles
3332
from utils.platform import Platform
33+
from utils.ddyaml import monkey_patch_pyyaml, yLoader
3434

3535
log = logging.getLogger('jmxfetch')
3636

@@ -488,6 +488,10 @@ def main(config_path=None):
488488
""" JMXFetch main entry point """
489489
confd_path, agent_config = init(config_path)
490490

491+
# do this early on
492+
if agent_config.get('disable_unsafe_yaml'):
493+
monkey_patch_pyyaml()
494+
491495
jmx = JMXFetch(confd_path, agent_config)
492496
return jmx.run()
493497

tests/core/test_utils_yaml.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# stdlib
2+
import os
3+
import unittest
4+
import tempfile
5+
6+
# project
7+
import yaml
8+
9+
from utils.ddyaml import (
10+
monkey_patch_pyyaml,
11+
monkey_patch_pyyaml_reverse,
12+
safe_yaml_dump_all,
13+
safe_yaml_load_all,
14+
safe_yaml_load,
15+
yDumper,
16+
)
17+
18+
FIXTURE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fixtures', 'checks')
19+
20+
class Dummy(object):
21+
def __init__(self):
22+
self.foo = 1
23+
self.bar = 'a'
24+
self.qux = {self.foo: self.bar}
25+
26+
def get_foo(self):
27+
return self.foo
28+
29+
def get_bar(self):
30+
return self.bar
31+
32+
def get_qux(self):
33+
return self.qux
34+
35+
36+
class UtilsYAMLTest(unittest.TestCase):
37+
38+
def setUp(self):
39+
monkey_patch_pyyaml()
40+
41+
def tearDown(self):
42+
monkey_patch_pyyaml_reverse()
43+
44+
def test_monkey_patch(self):
45+
self.assertTrue(yaml.dump_all == safe_yaml_dump_all)
46+
self.assertTrue(yaml.load_all == safe_yaml_load_all)
47+
self.assertTrue(yaml.load == safe_yaml_load)
48+
49+
def test_load(self):
50+
conf = os.path.join(FIXTURE_PATH, "valid_conf.yaml")
51+
with open(conf) as f:
52+
stream = f.read()
53+
54+
yaml_config_safe = safe_yaml_load(stream)
55+
yaml_config_native = yaml.load(stream)
56+
self.assertTrue(yaml_config_safe is not None)
57+
self.assertTrue(yaml_config_native is not None)
58+
self.assertTrue(yaml_config_native == yaml_config_safe)
59+
60+
yaml_config_safe = [entry for entry in safe_yaml_load_all(stream)]
61+
yaml_config_native = [entry for entry in yaml.load_all(stream)]
62+
self.assertTrue(yaml_config_safe is not [])
63+
self.assertTrue(yaml_config_native is not [])
64+
self.assertTrue(len(yaml_config_safe) == len(yaml_config_native))
65+
for safe, native in zip(yaml_config_safe, yaml_config_native):
66+
self.assertTrue(safe == native)
67+
68+
def test_unsafe(self):
69+
dummy = Dummy()
70+
71+
with self.assertRaises(yaml.representer.RepresenterError):
72+
yaml.dump_all([dummy])
73+
74+
with self.assertRaises(yaml.representer.RepresenterError):
75+
yaml.dump(dummy, Dumper=yDumper)
76+
77+
# reverse monkey patch and try again
78+
monkey_patch_pyyaml_reverse()
79+
80+
with tempfile.TemporaryFile(suffix='.yaml') as f:
81+
yaml.dump_all([dummy], stream=f)
82+
f.seek(0) # rewind
83+
84+
doc_unsafe = yaml.load(f)
85+
self.assertTrue(type(doc_unsafe) is Dummy)
86+
87+
monkey_patch_pyyaml()
88+
with self.assertRaises(yaml.constructor.ConstructorError):
89+
f.seek(0) # rewind
90+
safe_yaml_load(f)
91+
92+
with self.assertRaises(yaml.constructor.ConstructorError):
93+
f.seek(0) # rewind
94+
yaml.load(f)

util.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,6 @@
1010

1111
# 3p
1212
import yaml # noqa, let's guess, probably imported somewhere
13-
try:
14-
from yaml import CSafeLoader as yLoader
15-
from yaml import CSafeDumper as yDumper
16-
except ImportError:
17-
# On source install C Extensions might have not been built
18-
from yaml import SafeLoader as yLoader # noqa, imported from here elsewhere
19-
from yaml import SafeDumper as yDumper # noqa, imported from here elsewhere
2013

2114
# These classes are now in utils/, they are just here for compatibility reasons,
2215
# if a user actually uses them in a custom check
@@ -26,6 +19,7 @@
2619
from utils.platform import Platform, get_os # noqa, see ^^^
2720
from utils.proxy import get_proxy # noqa, see ^^^
2821
from utils.timer import Timer # noqa, see ^^^
22+
from utils.ddyaml import yLoader
2923

3024
COLON_NON_WIN_PATH = re.compile(':(?!\\\\)')
3125

@@ -104,6 +98,7 @@ def get_next_id(name):
10498
class NoInstancesFound(Exception):
10599
pass
106100

101+
107102
def check_yaml(conf_path):
108103
with open(conf_path) as f:
109104
check_config = yaml.load(f.read(), Loader=yLoader)

utils/ddyaml.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# (C) Datadog, Inc. 2011-2016
2+
# All rights reserved
3+
# Licensed under Simplified BSD License (see LICENSE)
4+
5+
# stdlib
6+
import logging
7+
8+
# 3p
9+
import yaml # noqa, let's guess, probably imported somewhere
10+
try:
11+
from yaml import CSafeLoader as yLoader
12+
from yaml import CSafeDumper as yDumper
13+
except ImportError:
14+
# On source install C Extensions might have not been built
15+
from yaml import SafeLoader as yLoader # noqa, imported from here elsewhere
16+
from yaml import SafeDumper as yDumper # noqa, imported from here elsewhere
17+
18+
log = logging.getLogger(__name__)
19+
20+
pyyaml_load = None
21+
pyyaml_load_all = None
22+
pyyaml_dump_all = None
23+
24+
25+
def safe_yaml_dump_all(documents, stream=None, Dumper=yDumper,
26+
default_style=None, default_flow_style=None,
27+
canonical=None, indent=None, width=None,
28+
allow_unicode=None, line_break=None,
29+
encoding='utf-8', explicit_start=None, explicit_end=None,
30+
version=None, tags=None):
31+
if Dumper != yDumper:
32+
log.warning("Unsafe dumping of YAML has been disabled - using safe dumper instead")
33+
34+
if pyyaml_dump_all:
35+
return pyyaml_dump_all(documents, stream, yDumper,
36+
default_style, default_flow_style,
37+
canonical, indent, width,
38+
allow_unicode, line_break,
39+
encoding, explicit_start, explicit_end,
40+
version, tags)
41+
42+
return yaml.dump_all(documents, stream, yDumper,
43+
default_style, default_flow_style,
44+
canonical, indent, width,
45+
allow_unicode, line_break,
46+
encoding, explicit_start, explicit_end,
47+
version, tags)
48+
49+
def safe_yaml_load(stream, Loader=yLoader):
50+
if Loader != yLoader:
51+
log.warning("Unsafe loading of YAML has been disabled - using safe loader instead")
52+
53+
if pyyaml_load:
54+
return pyyaml_load(stream, Loader=yLoader)
55+
56+
return yaml.load(stream, Loader=yLoader)
57+
58+
def safe_yaml_load_all(stream, Loader=yLoader):
59+
if Loader != yLoader:
60+
log.warning("Unsafe loading of YAML has been disabled - using safe loader instead")
61+
62+
if pyyaml_load_all:
63+
return pyyaml_load_all(stream, Loader=yLoader)
64+
65+
return yaml.load_all(stream, Loader=yLoader)
66+
67+
def monkey_patch_pyyaml():
68+
global pyyaml_load
69+
global pyyaml_load_all
70+
global pyyaml_dump_all
71+
72+
if not pyyaml_load:
73+
log.info("monkey patching yaml.load...")
74+
pyyaml_load = yaml.load
75+
yaml.load = safe_yaml_load
76+
if not pyyaml_load_all:
77+
log.info("monkey patching yaml.load_all...")
78+
pyyaml_load_all = yaml.load_all
79+
yaml.load_all = safe_yaml_load_all
80+
if not pyyaml_dump_all:
81+
log.info("monkey patching yaml.dump_all... (affects all yaml dump operations)")
82+
pyyaml_dump_all = yaml.dump_all
83+
yaml.dump_all = safe_yaml_dump_all
84+
85+
def monkey_patch_pyyaml_reverse():
86+
global pyyaml_load
87+
global pyyaml_load_all
88+
global pyyaml_dump_all
89+
90+
if pyyaml_load:
91+
log.info("reversing monkey patch for yaml.load...")
92+
yaml.load = pyyaml_load
93+
pyyaml_load = None
94+
if pyyaml_load_all:
95+
log.info("reversing monkey patch for yaml.load_all...")
96+
yaml.load_all = pyyaml_load_all
97+
pyyaml_load_all = None
98+
if pyyaml_dump_all:
99+
log.info("reversing monkey patch for yaml.dump_all... (affects all yaml dump operations)")
100+
yaml.dump_all = pyyaml_dump_all
101+
pyyaml_dump_all = None

utils/jmx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313

1414
# datadog
1515
from config import _windows_commondata_path, get_confd_path, JMX_VERSION
16-
from util import yDumper
1716
from utils.pidfile import PidFile
1817
from utils.platform import Platform
18+
from utils.ddyaml import yDumper
1919

2020
# JMXFetch java version
2121
JMX_FETCH_JAR_NAME = "jmxfetch-{ver}-jar-with-dependencies.jar".format(ver=JMX_VERSION)

win32/gui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@
8484
get_logging_config,
8585
get_version
8686
)
87-
from util import yLoader
8887
from utils.flare import Flare
8988
from utils.platform import Platform
89+
from utils.ddyaml import yLoader
9090

9191
# Constants describing the agent state
9292
AGENT_RUNNING = 0

0 commit comments

Comments
 (0)