From d72c652013f1e6a33c75580075a93357587ffde5 Mon Sep 17 00:00:00 2001 From: Graham Hayes Date: Wed, 4 Apr 2018 16:54:16 +0100 Subject: [PATCH] Ensure we do not pass invalid data for A records DNSPython does not allow IPs to have leading zeros for A records [1] and eventlet agrees [2], so we should ensure we do not create a situation where a project can DOS itself. This patch uses the DNSPython method to ensure it is kept in sync 1 - https://github.com/rthalley/dnspython/blob/v1.15.0/dns/ipv4.py#L52-L54 2 - https://github.com/eventlet/eventlet/blob/v0.20.0/eventlet/support/dns/ipv4.py#L52-L54 Partial-Bug: 1760833 Change-Id: I975b18d390647de9fe11c105cd421b761f88be6c --- contrib/fixleadingzeros.py | 127 ++++++++++++++++++ designate/objects/fields.py | 8 +- .../tests/unit/test_objects/test_rrdata_a.py | 32 +++++ 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100755 contrib/fixleadingzeros.py create mode 100644 designate/tests/unit/test_objects/test_rrdata_a.py diff --git a/contrib/fixleadingzeros.py b/contrib/fixleadingzeros.py new file mode 100755 index 000000000..8acfed241 --- /dev/null +++ b/contrib/fixleadingzeros.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# Copyright (C) 2018 Verizon +# +# Author: Graham Hayes +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys +import argparse +import logging +import dns.exception +from dns.ipv4 import inet_aton +import netaddr + +from designateclient.v2 import client +from designateclient import shell + +from keystoneauth1.identity import generic +from keystoneauth1 import session as keystone_session + + +auth = generic.Password( + auth_url=shell.env('OS_AUTH_URL'), + username=shell.env('OS_USERNAME'), + password=shell.env('OS_PASSWORD'), + project_name=shell.env('OS_PROJECT_NAME'), + project_domain_id=shell.env('OS_PROJECT_DOMAIN_ID'), + user_domain_id=shell.env('OS_USER_DOMAIN_ID')) + +session = keystone_session.Session(auth=auth) + +client = client.Client(session=session) + +logging.basicConfig() +LOG = logging.getLogger('fixleadingzeros') + + +def find_bad_recordsets(): + bad_recordsets = {} + LOG.debug("Looking for all A recordsets") + recordsets = client.recordsets.list_all_zones(criterion={'type': 'A', }) + LOG.debug("Found %d A recordsets", len(recordsets)) + LOG.debug("Filtering recordsets") + for recordset in recordsets: + for record in recordset['records']: + try: + inet_aton(record) + except dns.exception.SyntaxError: + bad_recordsets[recordset['id']] = recordset + LOG.debug("Found %d A invaild recordsets", len(bad_recordsets)) + return bad_recordsets + + +def show_recordsets(recordsets): + for rs in recordsets: + LOG.info( + ("%(name)s - %(records)s - Zone ID: %(zone_id)s - " + "Project ID: %(project_id)s ") % recordsets[rs]) + + +def fix_bad_recordsets(bad_recordsets): + LOG.debug("Removing leading zeros in IPv4 addresses") + for rs in bad_recordsets: + new_records = [] + for ip in bad_recordsets[rs]['records']: + new_records.append( + str(netaddr.IPAddress(ip, flags=netaddr.ZEROFILL).ipv4()) + ) + bad_recordsets[rs]['records'] = new_records + return bad_recordsets + + +def update_recordsets(recordsets): + LOG.info("Updating recordsets") + for rs in recordsets: + LOG.debug(("Updating %(name)s - %(records)s - Zone ID: %(zone_id)s - " + "Project ID: %(project_id)s ") % recordsets[rs]) + client.recordsets.update( + recordsets[rs]['zone_id'], + recordsets[rs]['id'], + {'records': recordsets[rs]['records']} + ) + + +def main(): + parser = argparse.ArgumentParser( + description='Fix any recordsets that have leading zeros in A records') + parser.add_argument('-v', '--verbose', action='store_true', + help='verbose output') + parser.add_argument('-d', '--dry-run', action='store_true', + help='do not modify records, just log bad records') + parser.add_argument('-a', '--all-projects', action='store_true', + help="Run on all projects") + args = parser.parse_args() + if args.verbose: + LOG.setLevel(logging.DEBUG) + else: + LOG.setLevel(logging.INFO) + + if args.all_projects: + client.session.all_projects = True + + bad_recordsets = find_bad_recordsets() + + LOG.info("Bad recordsets") + show_recordsets(bad_recordsets) + + fixed_recordsets = fix_bad_recordsets(bad_recordsets) + LOG.info("Fixed recordsets") + show_recordsets(fixed_recordsets) + + if not args.dry_run: + update_recordsets(fixed_recordsets) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/designate/objects/fields.py b/designate/objects/fields.py index 8901b1aae..0fa620485 100644 --- a/designate/objects/fields.py +++ b/designate/objects/fields.py @@ -12,7 +12,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +from dns import ipv4 +import dns.exception import re import uuid @@ -141,6 +142,11 @@ class ObjectField(ovoo_fields.ObjectField): class IPV4AddressField(ovoo_fields.IPV4AddressField): def coerce(self, obj, attr, value): + try: + # make sure that DNS Python agrees that it is a valid IP address + ipv4.inet_aton(str(value)) + except dns.exception.SyntaxError: + raise ValueError() value = super(IPV4AddressField, self).coerce(obj, attr, value) # we use this field as a string, not need a netaddr.IPAdress # as oslo.versionedobjects is using diff --git a/designate/tests/unit/test_objects/test_rrdata_a.py b/designate/tests/unit/test_objects/test_rrdata_a.py new file mode 100644 index 000000000..7bec8f2e9 --- /dev/null +++ b/designate/tests/unit/test_objects/test_rrdata_a.py @@ -0,0 +1,32 @@ +# Copyright 2018 Verizon Wireless +# +# Author: Graham Hayes +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from oslo_log import log as logging +import oslotest.base +import testtools + +from designate import exceptions + +from designate import objects + +LOG = logging.getLogger(__name__) + + +class RRDataATest(oslotest.base.BaseTestCase): + + def test_reject_leading_zeros(self): + record = objects.A(data='10.0.001.1') + with testtools.ExpectedException(exceptions.InvalidObject): + record.validate()