Commit message: Fix: acl: Do not delay evaluation of added nodes in some situations

It is not appropriate when the node has no children as it is not a
placeholder
CVE: CVE-2015-1867
CWE: CWE-264
Vulnerability description: Pacemaker before 1.1.13 does not properly evaluate added nodes, which allows remote read-only users to gain privileges via an acl command.

Project: pacemaker
File: xml.c

static inline bool TRACKING_CHANGES(xmlNode *xml)
{
    if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
        return FALSE;
    } else if(is_set(((xml_private_t *)xml->doc->_private)->flags, xpf_tracking)) {
        return TRUE;
    }
    return FALSE;
}

Project: pacemaker
File: xml.c

__xml_accept_changes(xmlNode * xml)
{
    xmlNode *cIter = NULL;
    xmlAttr *pIter = NULL;
    xml_private_t *p = xml->_private;

    p->flags = xpf_none;
    pIter = crm_first_attr(xml);

    while (pIter != NULL) {
        const xmlChar *name = pIter->name;

        p = pIter->_private;
        pIter = pIter->next;

        if(p->flags & xpf_deleted) {
            xml_remove_prop(xml, (const char *)name);

        } else {
            p->flags = xpf_none;
        }
    }

    for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) {
        __xml_accept_changes(cIter);
    }
}

Project: pacemaker
File: xml.c

__xml_acl_apply(xmlNode *xml) 
{
    GListPtr aIter = NULL;
    xml_private_t *p = NULL;
    xmlXPathObjectPtr xpathObj = NULL;

    if(xml_acl_enabled(xml) == FALSE) {
        p = xml->doc->_private;
        crm_trace("Not applying ACLs for %s", p->user);
        return;
    }

    p = xml->doc->_private;
    for(aIter = p->acls; aIter != NULL; aIter = aIter->next) {
        int max = 0, lpc = 0;
        xml_acl_t *acl = aIter->data;

        xpathObj = xpath_search(xml, acl->xpath);
        max = numXpathResults(xpathObj);

        for(lpc = 0; lpc < max; lpc++) {
            xmlNode *match = getXpathResult(xpathObj, lpc);
            char *path = xml_get_path(match);

            p = match->_private;
            crm_trace("Applying %x to %s for %s", acl->mode, path, acl->xpath);

#ifdef SUSE_ACL_COMPAT
            if(is_not_set(p->flags, acl->mode)) {
                if(is_set(p->flags, xpf_acl_read)
                   || is_set(p->flags, xpf_acl_write)
                   || is_set(p->flags, xpf_acl_deny)) {
                    crm_config_warn("Configuration element %s is matched by multiple ACL rules, only the first applies ('%s' wins over '%s')",
                                    path, __xml_acl_to_text(p->flags), __xml_acl_to_text(acl->mode));
                    free(path);
                    continue;
                }
            }
#endif

            p->flags |= acl->mode;
            free(path);
        }
        crm_trace("Now enforcing ACL: %s (%d matches)", acl->xpath, max);
        freeXpathObject(xpathObj);
    }

    p = xml->_private;
    if(is_not_set(p->flags, xpf_acl_read) && is_not_set(p->flags, xpf_acl_write)) {
        p->flags |= xpf_acl_deny;
        p = xml->doc->_private;
        crm_info("Enforcing default ACL for %s to %s", p->user, crm_element_name(xml));
    }

}

Project: pacemaker
File: xml.c

__xml_acl_check(xmlNode *xml, const char *name, enum xml_private_flags mode)
{
    CRM_ASSERT(xml);
    CRM_ASSERT(xml->doc);
    CRM_ASSERT(xml->doc->_private);

#if ENABLE_ACL
    {
        if(TRACKING_CHANGES(xml) && xml_acl_enabled(xml)) {
            int offset = 0;
            xmlNode *parent = xml;
            char buffer[XML_BUFFER_SIZE];
            xml_private_t *docp = xml->doc->_private;

            if(docp->acls == NULL) {
                crm_trace("Ordinary user %s cannot access the CIB without any defined ACLs", docp->user);
                set_doc_flag(xml, xpf_acl_denied);
                return FALSE;
            }

            offset = __get_prefix(NULL, xml, buffer, offset);
            if(name) {
                offset += snprintf(buffer + offset, XML_BUFFER_SIZE - offset, "[@%s]", name);
            }
            CRM_LOG_ASSERT(offset > 0);

            /* Walk the tree upwards looking for xml_acl_* flags
             * - Creating an attribute requires write permissions for the node
             * - Creating a child requires write permissions for the parent
             */

            if(name) {
                xmlAttr *attr = xmlHasProp(xml, (const xmlChar *)name);

                if(attr && mode == xpf_acl_create) {
                    mode = xpf_acl_write;
                }
            }

            while(parent && parent->_private) {
                xml_private_t *p = parent->_private;
                if(__xml_acl_mode_test(p->flags, mode)) {
                    return TRUE;

                } else if(is_set(p->flags, xpf_acl_deny)) {
                    crm_trace("%x access denied to %s: parent", mode, buffer);
                    set_doc_flag(xml, xpf_acl_denied);
                    return FALSE;
                }
                parent = parent->parent;
            }

            crm_trace("%x access denied to %s: default", mode, buffer);
            set_doc_flag(xml, xpf_acl_denied);
            return FALSE;
        }
    }
#endif

    return TRUE;
}

Project: pacemaker
File: xml.c

__xml_build_changes(xmlNode * xml, xmlNode *patchset)
{
    xmlNode *cIter = NULL;
    xmlAttr *pIter = NULL;
    xmlNode *change = NULL;
    xml_private_t *p = xml->_private;

    if(patchset && is_set(p->flags, xpf_created)) {
        int offset = 0;
        char buffer[XML_BUFFER_SIZE];

        if(__get_prefix(NULL, xml->parent, buffer, offset) > 0) {
            int position = __xml_offset_no_deletions(xml);

            change = create_xml_node(patchset, XML_DIFF_CHANGE);

            crm_xml_add(change, XML_DIFF_OP, "create");
            crm_xml_add(change, XML_DIFF_PATH, buffer);
            crm_xml_add_int(change, XML_DIFF_POSITION, position);
            add_node_copy(change, xml);
        }

        return;
    }

    for (pIter = crm_first_attr(xml); pIter != NULL; pIter = pIter->next) {
        xmlNode *attr = NULL;

        p = pIter->_private;
        if(is_not_set(p->flags, xpf_deleted) && is_not_set(p->flags, xpf_dirty)) {
            continue;
        }

        if(change == NULL) {
            int offset = 0;
            char buffer[XML_BUFFER_SIZE];

            if(__get_prefix(NULL, xml, buffer, offset) > 0) {
                change = create_xml_node(patchset, XML_DIFF_CHANGE);

                crm_xml_add(change, XML_DIFF_OP, "modify");
                crm_xml_add(change, XML_DIFF_PATH, buffer);

                change = create_xml_node(change, XML_DIFF_LIST);
            }
        }

        attr = create_xml_node(change, XML_DIFF_ATTR);

        crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name);
        if(p->flags & xpf_deleted) {
            crm_xml_add(attr, XML_DIFF_OP, "unset");

        } else {
            const char *value = crm_element_value(xml, (const char *)pIter->name);

            crm_xml_add(attr, XML_DIFF_OP, "set");
            crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value);
        }
    }

    if(change) {
        xmlNode *result = NULL;

        change = create_xml_node(change->parent, XML_DIFF_RESULT);
        result = create_xml_node(change, (const char *)xml->name);

        for (pIter = crm_first_attr(xml); pIter != NULL; pIter = pIter->next) {
            const char *value = crm_element_value(xml, (const char *)pIter->name);

            p = pIter->_private;
            if (is_not_set(p->flags, xpf_deleted)) {
                crm_xml_add(result, (const char *)pIter->name, value);
            }
        }
    }

    for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) {
        __xml_build_changes(cIter, patchset);
    }

    p = xml->_private;
    if(patchset && is_set(p->flags, xpf_moved)) {
        int offset = 0;
        char buffer[XML_BUFFER_SIZE];

        crm_trace("%s.%s moved to position %d", xml->name, ID(xml), __xml_offset(xml));
        if(__get_prefix(NULL, xml, buffer, offset) > 0) {
            change = create_xml_node(patchset, XML_DIFF_CHANGE);

            crm_xml_add(change, XML_DIFF_OP, "move");
            crm_xml_add(change, XML_DIFF_PATH, buffer);
            crm_xml_add_int(change, XML_DIFF_POSITION, __xml_offset_no_deletions(xml));
        }
    }
}

Project: pacemaker
File: xml.c

add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * update, gboolean as_diff)
{
    xmlNode *a_child = NULL;
    const char *object_id = NULL;
    const char *object_name = NULL;

#if XML_PARSE_DEBUG
    crm_log_xml_trace("update:", update);
    crm_log_xml_trace("target:", target);
#endif

    CRM_CHECK(update != NULL, return 0);

    if (update->type == XML_COMMENT_NODE) {
        return add_xml_comment(parent, target, update);
    }

    object_name = crm_element_name(update);
    object_id = ID(update);

    CRM_CHECK(object_name != NULL, return 0);

    if (target == NULL && object_id == NULL) {
        /*  placeholder object */
        target = find_xml_node(parent, object_name, FALSE);

    } else if (target == NULL) {
        target = find_entity(parent, object_name, object_id);
    }

    if (target == NULL) {
        target = create_xml_node(parent, object_name);
        CRM_CHECK(target != NULL, return 0);
#if XML_PARSER_DEBUG
        crm_trace("Added  <%s%s%s/>", crm_str(object_name),
                  object_id ? " id=" : "", object_id ? object_id : "");

    } else {
        crm_trace("Found node <%s%s%s/> to update",
                  crm_str(object_name), object_id ? " id=" : "", object_id ? object_id : "");
#endif
    }

    CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(update)), return 0);

    if (as_diff == FALSE) {
        /* So that expand_plus_plus() gets called */
        copy_in_properties(target, update);

    } else {
        /* No need for expand_plus_plus(), just raw speed */
        xmlAttrPtr pIter = NULL;

        for (pIter = crm_first_attr(update); pIter != NULL; pIter = pIter->next) {
            const char *p_name = (const char *)pIter->name;
            const char *p_value = crm_attr_value(pIter);

            /* Remove it first so the ordering of the update is preserved */
            xmlUnsetProp(target, (const xmlChar *)p_name);
            xmlSetProp(target, (const xmlChar *)p_name, (const xmlChar *)p_value);
        }
    }

    for (a_child = __xml_first_child(update); a_child != NULL; a_child = __xml_next(a_child)) {
#if XML_PARSER_DEBUG
        crm_trace("Updating child <%s id=%s>", crm_element_name(a_child), ID(a_child));
#endif
        add_xml_object(target, NULL, a_child, as_diff);
    }

#if XML_PARSER_DEBUG
    crm_trace("Finished with <%s id=%s>", crm_str(object_name), crm_str(object_id));
#endif
    return 0;
}

Project: pacemaker
File: xml.c

crm_xml_replace(xmlNode * node, const char *name, const char *value)
{
    bool dirty = FALSE;
    xmlAttr *attr = NULL;
    const char *old_value = NULL;

    CRM_CHECK(node != NULL, return NULL);
    CRM_CHECK(name != NULL && name[0] != 0, return NULL);

    old_value = crm_element_value(node, name);

    /* Could be re-setting the same value */
    CRM_CHECK(old_value != value, return value);

    if(__xml_acl_check(node, name, xpf_acl_write) == FALSE) {
        /* Create a fake object linked to doc->_private instead? */
        crm_trace("Cannot replace %s=%s to %s", name, value, node->name);
        return NULL;

    } else if (old_value != NULL && value == NULL) {
        xml_remove_prop(node, name);
        return NULL;

    } else if (value == NULL) {
        return NULL;
    }

    if(TRACKING_CHANGES(node)) {
        if(old_value == NULL || value == NULL || strcmp(old_value, value) != 0) {
            dirty = TRUE;
        }
    }

    attr = xmlSetProp(node, (const xmlChar *)name, (const xmlChar *)value);
    if(dirty) {
        crm_attr_dirty(attr);
    }
    CRM_CHECK(attr && attr->children && attr->children->content, return NULL);
    return (char *)attr->children->content;
}

Project: pacemaker
File: xml.c

freeXpathObject(xmlXPathObjectPtr xpathObj)
{
    int lpc, max = numXpathResults(xpathObj);

    if(xpathObj == NULL) {
        return;
    }

    for(lpc = 0; lpc < max; lpc++) {
        if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) {
            xpathObj->nodesetval->nodeTab[lpc] = NULL;
        }
    }

    /* _Now_ its safe to free it */
    xmlXPathFreeObject(xpathObj);
}

Project: pacemaker
File: xml.c

get_message_xml(xmlNode * msg, const char *field)
{
    xmlNode *tmp = first_named_child(msg, field);

    return __xml_first_child(tmp);
}

Project: pacemaker
File: xml.c

get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
{
    int max;
    xmlNode *result = NULL;
    xmlXPathObjectPtr xpathObj = NULL;
    char *nodePath = NULL;
    char *matchNodePath = NULL;

    if (xpath == NULL) {
        return xml_obj;         /* or return NULL? */
    }

    xpathObj = xpath_search(xml_obj, xpath);
    nodePath = (char *)xmlGetNodePath(xml_obj);
    max = numXpathResults(xpathObj);

    if (max < 1) {
        do_crm_log(error_level, "No match for %s in %s", xpath, crm_str(nodePath));
        crm_log_xml_explicit(xml_obj, "Unexpected Input");

    } else if (max > 1) {
        int lpc = 0;

        do_crm_log(error_level, "Too many matches for %s in %s", xpath, crm_str(nodePath));

        for (lpc = 0; lpc < max; lpc++) {
            xmlNode *match = getXpathResult(xpathObj, lpc);

            CRM_LOG_ASSERT(match != NULL);
            if(match != NULL) {
                matchNodePath = (char *)xmlGetNodePath(match);
                do_crm_log(error_level, "%s[%d] = %s", xpath, lpc, crm_str(matchNodePath));
                free(matchNodePath);
            }
        }
        crm_log_xml_explicit(xml_obj, "Bad Input");

    } else {
        result = getXpathResult(xpathObj, 0);
    }

    freeXpathObject(xpathObj);
    free(nodePath);

    return result;
}

Project: pacemaker
File: xml.c

hash2nvpair(gpointer key, gpointer value, gpointer user_data)
{
    const char *name = key;
    const char *s_value = value;

    xmlNode *xml_node = user_data;
    xmlNode *xml_child = create_xml_node(xml_node, XML_CIB_TAG_NVPAIR);

    crm_xml_add(xml_child, XML_ATTR_ID, name);
    crm_xml_add(xml_child, XML_NVPAIR_ATTR_NAME, name);
    crm_xml_add(xml_child, XML_NVPAIR_ATTR_VALUE, s_value);

    crm_trace("dumped: name=%s value=%s", name, s_value);
}

Project: pacemaker
File: xml.c

insert_prefix(int options, char **buffer, int *offset, int *max, int depth)
{
    if (options & xml_log_option_formatted) {
        size_t spaces = 2 * depth;

        if ((*buffer) == NULL || spaces >= ((*max) - (*offset))) {
            (*max) = QB_MAX(CHUNK_SIZE, (*max) * 2);
            (*buffer) = realloc_safe((*buffer), (*max) + 1);
        }
        memset((*buffer) + (*offset), ' ', spaces);
        (*offset) += spaces;
    }
}

Project: pacemaker
File: xml.c

pcmkRegisterNode(xmlNodePtr node)
{
    xml_private_t *p = NULL;

    switch(node->type) {
        case XML_ELEMENT_NODE:
        case XML_DOCUMENT_NODE:
        case XML_ATTRIBUTE_NODE:
        case XML_COMMENT_NODE:
            p = calloc(1, sizeof(xml_private_t));
            p->check = XML_PRIVATE_MAGIC;
            /* Flags will be reset if necessary when tracking is enabled */
            p->flags |= (xpf_dirty|xpf_created);
            node->_private = p;
            break;
        case XML_TEXT_NODE:
        case XML_DTD_NODE:
            break;
        default:
            /* Ignore */
            crm_trace("Ignoring %p %d", node, node->type);
            CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
            break;
    }

    if(p && TRACKING_CHANGES(node)) {
        /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is
         * not hooked up at the point we are called
         */
        set_doc_flag(node, xpf_dirty);
        __xml_node_dirty(node);
    }
}

Project: pacemaker
File: xml.c

set_doc_flag(xmlNode *xml, long flag) 
{

    if(xml && xml->doc && xml->doc->_private){
        /* During calls to xmlDocCopyNode(), xml->doc may be unset */
        xml_private_t *p = xml->doc->_private;

        p->flags |= flag;
        /* crm_trace("Setting flag %x due to %s[@id=%s]", flag, xml->name, ID(xml)); */
    }
}

Project: pacemaker
File: xml.c

xml_acl_denied(xmlNode *xml) 
{
    if(xml && xml->doc && xml->doc->_private){
        xml_private_t *p = xml->doc->_private;

        return is_set(p->flags, xpf_acl_denied);
    }
    return FALSE;
}

Project: pacemaker
File: xml.c

xml_create_patchset(int format, xmlNode *source, xmlNode *target, bool *config_changed, bool manage_version, bool with_digest)
{
    int counter = 0;
    bool config = FALSE;
    xmlNode *patch = NULL;
    const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION);

    xml_acl_disable(target);
    if(xml_document_dirty(target) == FALSE) {
        crm_trace("No change %d", format);
        return NULL; /* No change */
    }

    config = is_config_change(target);
    if(config_changed) {
        *config_changed = config;
    }

    if(manage_version && config) {
        crm_trace("Config changed %d", format);
        crm_xml_add(target, XML_ATTR_NUMUPDATES, "0");

        crm_element_value_int(target, XML_ATTR_GENERATION, &counter);
        crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1);

    } else if(manage_version) {
        crm_trace("Status changed %d", format);
        crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter);
        crm_xml_add_int(target, XML_ATTR_NUMUPDATES, counter+1);
    }

    if(format == 0) {
        if(patch_legacy_mode()) {
            format = 1;

        } else if(compare_version("3.0.8", version) < 0) {
            format = 2;

        } else {
            format = 1;
        }
        crm_trace("Using patch format %d for version: %s", format, version);
    }

    switch(format) {
        case 1:
            with_digest = TRUE;
            patch = xml_create_patchset_v1(source, target, config, with_digest);
            break;
        case 2:
            patch = xml_create_patchset_v2(source, target);
            break;
        default:
            crm_err("Unknown patch format: %d", format);
            return NULL;
    }

    if(patch && with_digest) {
        char *digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version);

        crm_xml_add(patch, XML_ATTR_DIGEST, digest);
        free(digest);
    }
    return patch;
}

Project: pacemaker
File: xml.c

xml_log(int priority, const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    qb_log_from_external_source_va(__FUNCTION__, __FILE__, fmt, priority, __LINE__, 0, ap);
    va_end(ap);
}

Project: pacemaker
File: xml.c

xpath_search(xmlNode * xml_top, const char *path)
{
    xmlDocPtr doc = NULL;
    xmlXPathObjectPtr xpathObj = NULL;
    xmlXPathContextPtr xpathCtx = NULL;
    const xmlChar *xpathExpr = (const xmlChar *)path;

    CRM_CHECK(path != NULL, return NULL);
    CRM_CHECK(xml_top != NULL, return NULL);
    CRM_CHECK(strlen(path) > 0, return NULL);

    doc = getDocPtr(xml_top);

    xpathCtx = xmlXPathNewContext(doc);
    CRM_ASSERT(xpathCtx != NULL);

    xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
    xmlXPathFreeContext(xpathCtx);
    return xpathObj;
}