diff --git a/python/samba/tests/samba_tool/sites.py b/python/samba/tests/samba_tool/sites.py index 81cc66d73b0..ee281094d2a 100644 --- a/python/samba/tests/samba_tool/sites.py +++ b/python/samba/tests/samba_tool/sites.py @@ -110,3 +110,27 @@ class SitesSubnetCmdTestCase(BaseSitesCmdTestCase): self.assertIsNotNone(ret) self.assertEqual(len(ret), 1) self.samdb.delete(dnsubnet, ["tree_delete:0"]) + + def test_site_subnet_create_should_fail(self): + cidrs = (("10.9.8.0/33", self.sitename), # mask too big + ("50.60.0.0/8", self.sitename2), # insufficient zeros + ("50.261.0.0/16", self.sitename2), # bad octet + ("7.0.0.0.0/0", self.sitename), # insufficient zeros + ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119", + self.sitename), # insufficient zeros + ) + + for cidr, sitename in cidrs: + result, out, err = self.runsubcmd("sites", "subnet", "create", + cidr, sitename, + "-H", self.dburl, + self.creds_string) + self.assertCmdFail(result) + + ret = self.samdb.search(base=self.config_dn, + scope=ldb.SCOPE_SUBTREE, + expression=('(&(objectclass=subnet)(cn=%s))' + % cidr)) + + self.assertIsNotNone(ret) + self.assertEqual(len(ret), 0) diff --git a/source4/dsdb/samdb/ldb_modules/samldb.c b/source4/dsdb/samdb/ldb_modules/samldb.c index 153c85ea571..91e9625bf40 100644 --- a/source4/dsdb/samdb/ldb_modules/samldb.c +++ b/source4/dsdb/samdb/ldb_modules/samldb.c @@ -42,6 +42,7 @@ #include "ldb_wrap.h" #include "param/param.h" #include "libds/common/flag_mapping.h" +#include "system/network.h" struct samldb_ctx; enum samldb_add_type { @@ -2644,6 +2645,253 @@ static int samldb_fsmo_role_owner_check(struct samldb_ctx *ac) return LDB_SUCCESS; } +/* + * Return zero if the number of zero bits in the address (looking from low to + * high) is equal to or greater than the length minus the mask. Otherwise it + * returns -1. + */ +static int check_cidr_zero_bits(uint8_t *address, unsigned int len, + unsigned int mask) +{ + /*
is an integer in big-endian form, bits long. All + bits between and must be zero. */ + int i; + unsigned int byte_len; + unsigned int byte_mask; + unsigned int bit_mask; + if (len == 32) { + DBG_INFO("Looking at address %02x%02x%02x%02x, mask %u\n", + address[0], address[1], address[2], address[3], + mask); + } else if (len == 128){ + DBG_INFO("Looking at address " + "%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x-%02x%02x-%02x%02x-%02x%02x, mask %u\n", + address[0], address[1], address[2], address[3], + address[4], address[5], address[6], address[7], + address[8], address[9], address[10], address[11], + address[12], address[13], address[14], address[15], + mask); + } + + if (mask > len){ + DBG_INFO("mask %u is too big (> %u)\n", mask, len); + return -1; + } + if (mask == len){ + /* single address subnet. + * In IPv4 all 255s is invalid by the bitmask != address rule + * in MS-ADTS. IPv6 does not suffer. + */ + if (len == 32){ + if (address[0] == 255 && + address[1] == 255 && + address[2] == 255 && + address[3] == 255){ + return -1; + } + } + return 0; + } + + byte_len = len / 8; + byte_mask = mask / 8; + + for (i = byte_len - 1; i > byte_mask; i--){ + DBG_DEBUG("checking byte %d %02x\n", i, address[i]); + if (address[i] != 0){ + return -1; + } + } + bit_mask = (1 << (8 - (mask & 7))) - 1; + DBG_DEBUG("checking bitmask %02x & %02x overlap %02x\n", bit_mask, address[byte_mask], + bit_mask & address[byte_mask]); + if (address[byte_mask] & bit_mask){ + return -1; + } + + /* According to MS-ADTS, the mask can't exactly equal the bitmask for + * IPv4 (but this is fine for v6). That is 255.255.80.0/17 is bad, + * because the bitmask implied by "/17" is 255.255.80.0. + * + * The bit_mask used in the previous check is the complement of what + * we want here. + */ + if (len == 32 && address[byte_mask] == (uint8_t)~bit_mask){ + bool ok = false; + for (i = 0; i < byte_mask; i++){ + if (address[i] != 255){ + ok = true; + break; + } + } + if (ok == false){ + return -1; + } + } + return 0; +} + + + +static int check_address_roundtrip(const char *address, int family, + const uint8_t *address_bytes, + char *buffer, int buffer_len) +{ + /* + * Check that the address is in the canonical RFC5952 format for IPv6, + * and lacks extra leading zeros for each dotted decimal for IPv4. + * Handily this is what inet_ntop() gives you. + */ + const char *address_redux = inet_ntop(family, address_bytes, + buffer, buffer_len); + if (address_redux == NULL){ + DBG_INFO("Address round trip %s failed unexpectedly" + " with errno %d\n", address, errno); + return -1; + } + if (strcasecmp(address, address_redux) != 0){ + DBG_INFO("Address %s round trips to %s; fail!\n", + address, address_redux); + return -1; + } + return 0; +} + + + +/* + * MS-ADTS v20150630 6.1.1.2.2.2.1 Subnet Object, refers to RFC1166 and + * RFC2373. It specifies something seemingly indistinguishable from an RFC4632 + * CIDR address range without saying so explicitly. Here we follow the CIDR + * spec. + * + * Return 0 on success, -1 on error. + */ +static int verify_cidr(const char *cidr) +{ + char *address = NULL, *slash = NULL, *endptr = NULL; + bool has_colon, has_dot; + int res, ret; + unsigned long mask; + uint8_t *address_bytes = NULL; + char *address_redux = NULL; + unsigned int address_len; + TALLOC_CTX *frame = NULL; + + DBG_DEBUG("CIDR is %s\n", cidr); + frame = talloc_stackframe(); + address = talloc_strdup(frame, cidr); + if (address == NULL){ + goto error; + } + + /* there must be a '/' */ + slash = strchr(address, '/'); + if (slash == NULL){ + goto error; + } + /* terminate the address for strchr, inet_pton */ + *slash = '\0'; + + mask = strtoul(slash + 1, &endptr, 10); + if (mask == 0){ + DBG_INFO("Windows does not like the zero mask, " + "so nor do we: %s\n", cidr); + goto error; + } + + if (*endptr != '\0' || endptr == slash + 1){ + DBG_INFO("CIDR mask is not a proper integer: %s\n", cidr); + goto error; + } + + address_bytes = talloc_size(frame, sizeof(struct in6_addr)); + if (address_bytes == NULL){ + goto error; + } + + address_redux = talloc_size(frame, INET6_ADDRSTRLEN); + if (address_redux == NULL){ + goto error; + } + + DBG_INFO("found address %s, mask %lu\n", address, mask); + has_colon = (strchr(address, ':') == NULL) ? false : true; + has_dot = (strchr(address, '.') == NULL) ? false : true; + if (has_dot && has_colon){ + /* This seems to be an IPv4 address embedded in IPv6, which is + icky. We don't support it. */ + DBG_INFO("Refusing to consider cidr '%s' with dots and colons\n", + cidr); + goto error; + } else if (has_colon){ /* looks like IPv6 */ + res = inet_pton(AF_INET6, address, address_bytes); + if (res != 1) { + DBG_INFO("Address in %s fails to parse as IPv6\n", cidr); + goto error; + } + address_len = 128; + if (check_address_roundtrip(address, AF_INET6, address_bytes, + address_redux, INET6_ADDRSTRLEN)){ + goto error; + } + } else if (has_dot) { + /* looks like IPv4 */ + if (strcmp(address, "0.0.0.0") == 0){ + DBG_INFO("Windows does not like the zero IPv4 address, " + "so nor do we.\n"); + goto error; + } + res = inet_pton(AF_INET, address, address_bytes); + if (res != 1) { + DBG_INFO("Address in %s fails to parse as IPv4\n", cidr); + goto error; + } + address_len = 32; + + if (check_address_roundtrip(address, AF_INET, address_bytes, + address_redux, INET_ADDRSTRLEN)){ + goto error; + } + } else { + /* This doesn't look like an IP address at all. */ + goto error; + } + + ret = check_cidr_zero_bits(address_bytes, address_len, mask); + talloc_free(frame); + return ret; + error: + talloc_free(frame); + return -1; +} + + +static int samldb_verify_subnet(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char *cidr = NULL; + const struct ldb_val *rdn_value = NULL; + + rdn_value = ldb_dn_get_rdn_val(ac->msg->dn); + cidr = ldb_dn_escape_value(ac, *rdn_value); + DBG_INFO("looking at cidr '%s'\n", cidr); + if (cidr == NULL) { + ldb_set_errstring(ldb, + "samldb: adding an empty subnet cidr seems wrong"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (verify_cidr(cidr)){ + ldb_set_errstring(ldb, + "samldb: subnet value is invalid"); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + return LDB_SUCCESS; +} + /* add */ static int samldb_add(struct ldb_module *module, struct ldb_request *req) @@ -2752,6 +3000,17 @@ static int samldb_add(struct ldb_module *module, struct ldb_request *req) return samldb_fill_object(ac); } + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "subnet") != NULL) { + ret = samldb_verify_subnet(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + /* We are just checking the value is valid, and there are no + values to fill in. */ + } + talloc_free(ac); /* nothing matched, go on */ @@ -3099,6 +3358,15 @@ static int check_rename_constraints(struct ldb_message *msg, return LDB_ERR_UNWILLING_TO_PERFORM; } + /* subnet objects */ + if (samdb_find_attribute(ldb, msg, "objectclass", "subnet") != NULL) { + ret = samldb_verify_subnet(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + /* systemFlags */ dn1 = ldb_dn_get_parent(ac, olddn); diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index 6242a9dbda0..1714ff814c7 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -183,5 +183,293 @@ class SimpleSubnetTests(SitesBaseTests): self.assertRaises(subnets.SubnetNotFound, subnets.delete_subnet, self.ldb, basedn, cidr) + def test_create_bad_ranges(self): + """These CIDR ranges all have something wrong with them, and they + should all fail.""" + basedn = self.ldb.get_config_basedn() + + cidrs = [ + # IPv4 + # insufficient zeros + "10.11.12.0/14", + "110.0.0.0/6", + "1.0.0.0/0", + "10.11.13.1/24", + "1.2.3.4/29", + "10.11.12.0/21", + # out of range mask + "110.0.0.0/33", + "110.0.0.0/-1", + "4.0.0.0/111", + # out of range address + "310.0.0.0/24", + "10.0.0.256/32", + "1.1.-20.0/24", + # badly formed + "1.0.0.0/1e", + "1.0.0.0/24.0", + "1.0.0.0/1/1", + "1.0.0.0", + "1.c.0.0/24", + "1.2.0.0.0/27", + "1.23.0/24", + "1.23.0.-7/24", + "1.-23.0.7/24", + "1.23.-0.7/24", + "1.23.0.0/0x10", + # IPv6 insufficient zeros -- this could be a subtle one + # due to the vagaries of endianness in the 16 bit groups. + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119", + "aaaa:bbbb::/31", + "a:b::/31", + "c000::/1", + "a::b00/119", + "1::1/127", + "1::2/126", + "1::100/119", + "1::8000/112", + # out of range mask + "a:b::/130", + "a:b::/-1", + "::/129", + # An IPv4 address can't be exactly the bitmask (MS ADTS) + "128.0.0.0/1", + "192.0.0.0/2", + "255.192.0.0/10", + "255.255.255.0/24", + "255.255.255.255/32", + "0.0.0.0/0", + # The address can't have leading zeros (not RFC 4632, but MS ADTS) + "00.1.2.0/24", + "003.1.2.0/24", + "022.1.0.0/16", + "00000000000000000000000003.1.2.0/24", + "09876::abfc/126", + "0aaaa:bbbb::/32", + "009876::abfc/126", + "000a:bbbb::/32", + + # How about extraneous zeros later on + "3.01.2.0/24", + "3.1.2.00/24", + "22.001.0.0/16", + "3.01.02.0/24", + "100a:0bbb:0023::/48", + "100a::0023/128", + + # Windows doesn't like the zero IPv4 address + "0.0.0.0/8", + # or the zero mask on IPv6 + "::/0", + + # various violations of RFC5952 + "0:0:0:0:0:0:0:0/8", + "0::0/0", + "::0:0/48", + "::0:4/128", + "0::/8", + "0::4f/128", + "0::42:0:0:0:0/64", + "4f::0/48", + + # badly formed -- mostly the wrong arrangement of colons + "a::b::0/120", + "a::abcdf:0/120", + "a::g:0/120", + "::0::3/48", + "2001:3::110::3/118", + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1111:0000/128", + "a:::5:0/120", + + # non-canonical representations (vs RFC 5952) + # "2001:0:c633:63::1:0/120" is correct + "2001:0:c633:63:0:0:1:0/120", + "2001::c633:63:0:0:1:0/120", + "2001:0:c633:63:0:0:1::/120", + + # "10:0:0:42::/64" is correct + "10::42:0:0:0:0/64", + "10:0:0:42:0:0:0:0/64", + + # "1::4:5:0:0:8/127" is correct + "1:0:0:4:5:0:0:8/127", + "1:0:0:4:5::8/127", + + # "2001:db8:0:1:1:1:1:1/128" is correct + "2001:db8::1:1:1:1:1/128", + + # IP4 embedded - rejected + "a::10.0.0.0/120", + "a::10.9.8.7/128", + "::ffff:0:0/96", #this one fails on WIN2012r2 + # completely wrong + None, + "bob", + 3.1415, + False, + "10.11.16.0/24\x00hidden bytes past a zero", + self, + ] + + failures = [] + for cidr in cidrs: + try: + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + except subnets.SubnetInvalid: + print >> sys.stderr, "%s fails properly" % (cidr,) + continue + + # we are here because it succeeded when it shouldn't have. + print >> sys.stderr, "CIDR %s fails to fail" % (cidr,) + failures.append(cidr) + subnets.delete_subnet(self.ldb, basedn, cidr) + + if failures: + print "These bad subnet names were accepted:" + for cidr in failures: + print " %s" % cidr + self.fail() + + def test_create_good_ranges(self): + """All of these CIDRs are good, and the subnet creation should + succeed.""" + basedn = self.ldb.get_config_basedn() + + cidrs = [ + # IPv4 + "10.11.12.0/24", + "10.11.12.0/23", + "10.11.12.0/25", + "110.0.0.0/7", + "1.0.0.0/32", + "10.11.13.0/32", + "10.11.13.1/32", + "99.0.97.0/24", + "1.2.3.4/30", + "10.11.12.0/22", + "0.12.13.0/24", + # IPv6 + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120", + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11f0/124", + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11fc/126", + # don't forget upper case + "FFFF:FFFF:FFFF:FFFF:ABCD:EfFF:FFFF:FFeF/128", + "9876::ab00/120", + "9876::abf0/124", + "9876::abfc/126", + "aaaa:bbbb::/32", + "aaaa:bbba::/31", + "aaaa:ba00::/23", + "aaaa:bb00::/24", + "aaaa:bb00::/77", + "::/48", + "a:b::/32", + "c000::/2", + "a::b00/120", + "1::2/127", + # this pattern of address suffix == mask is forbidden with + # IPv4 but OK for IPv6. + "8000::/1", + "c000::/2", + "ffff:ffff:ffc0::/42", + "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF/128", + # leading zeros are forbidden, but implicit IPv6 zeros + # (via "::") are OK. + "::1000/116", + "::8000/113", + # taken to the logical conclusion, "::/0" should be OK, but no. + "::/48", + + # Try some reserved ranges, which it might be reasonable + # to exclude, but which are not excluded in practice. + "129.0.0.0/16", + "129.255.0.0/16", + "100.64.0.0/10", + "127.0.0.0/8", + "127.0.0.0/24", + "169.254.0.0/16", + "169.254.1.0/24", + "192.0.0.0/24", + "192.0.2.0/24", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "130.129.0.0/16", + "130.255.0.0/16", + "192.12.0.0/24", + "223.255.255.0/24", + "240.255.255.0/24", + "224.0.0.0/8", + "::/96", + "100::/64", + "2001:10::/28", + "fec0::/10", + "ff00::/8", + "::1/128", + "2001:db8::/32", + "2001:10::/28", + "2002::/24", + "2002:a00::/24", + "2002:7f00::/24", + "2002:a9fe::/32", + "2002:ac10::/28", + "2002:c000::/40", + "2002:c000:200::/40", + "2002:c0a8::/32", + "2002:c612::/31", + "2002:c633:6400::/40", + "2002:cb00:7100::/40", + "2002:e000::/20", + "2002:f000::/20", + "2002:ffff:ffff::/48", + "2001::/40", + "2001:0:a00::/40", + "2001:0:7f00::/40", + "2001:0:a9fe::/48", + "2001:0:ac10::/44", + "2001:0:c000::/56", + "2001:0:c000:200::/56", + "2001:0:c0a8::/48", + "2001:0:c612::/47", + "2001:0:c633:6400::/56", + "2001:0:cb00:7100::/56", + "2001:0:e000::/36", + "2001:0:f000::/36", + "2001:0:ffff:ffff::/64", + + # non-RFC-5952 versions of these are tested in create_bad_ranges + "2001:0:c633:63::1:0/120", + "10:0:0:42::/64", + "1::4:5:0:0:8/127", + "2001:db8:0:1:1:1:1:1/128", + ] + failures = [] + + for cidr in cidrs: + try: + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + except subnets.SubnetInvalid, e: + print e + failures.append(cidr) + continue + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression=('(&(objectclass=subnet)(cn=%s))' % + cidr)) + + if len(ret) != 1: + print "%s was not created" % cidr + failures.append(cidr) + continue + subnets.delete_subnet(self.ldb, basedn, cidr) + + if failures: + print "These good subnet names were not accepted:" + for cidr in failures: + print " %s" % cidr + self.fail() + + TestProgram(module=__name__, opts=subunitopts)