Skip to content

Restrict DAPI callable resolution to exposed resources only#34889

Open
vikman90 wants to merge 6 commits into4.14.4from
fix/4169-cluster-dapi-filter-callables
Open

Restrict DAPI callable resolution to exposed resources only#34889
vikman90 wants to merge 6 commits into4.14.4from
fix/4169-cluster-dapi-filter-callables

Conversation

@vikman90
Copy link
Member

Description

This change hardens DAPI callable resolution by allowing remote execution only for functions explicitly exposed through @expose_resources.

The update reduces the internal callable surface available through DAPI and ensures that low-level helper functions that are not intended for remote execution are rejected during deserialization.

Credit to @alimezar and @maru for reporting and helping identify this issue.

Proposed Changes

  • Validate callable resolution in as_wazuh_object() and accept only functions explicitly exposed with @expose_resources
  • Reject non-exposed internal functions during DAPI deserialization
  • Update test_as_wazuh_object_ok to cover both exposed and non-exposed functions
  • Keep the existing behavior for legitimate exposed functions

Results and Evidence

Unit test passing:

============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0 -- /root/wazuh-4.14.4/venv_test/bin/python3
cachedir: .pytest_cache
rootdir: /root/wazuh-4.14.4/framework
configfile: pytest.ini
plugins: asyncio-1.3.0, cov-7.0.0, anyio-4.12.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collecting ... collected 1 item

framework/wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok PASSED [100%]

=============================== warnings summary ===============================
venv_test/lib/python3.12/site-packages/connexion/json_schema.py:16
venv_test/lib/python3.12/site-packages/connexion/json_schema.py:16
  /root/wazuh-4.14.4/venv_test/lib/python3.12/site-packages/connexion/json_schema.py:16: DeprecationWarning: jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the https://github.com/python-jsonschema/referencing library, which provides more compliant referencing behavior as well as more flexible APIs for customization. A future release will remove RefResolver. Please file a feature request (on referencing) if you are missing an API for the kind of customization you need.
    from jsonschema import Draft4Validator, RefResolver

venv_test/lib/python3.12/site-packages/connexion/json_schema.py:17
  /root/wazuh-4.14.4/venv_test/lib/python3.12/site-packages/connexion/json_schema.py:17: DeprecationWarning: jsonschema.exceptions.RefResolutionError is deprecated as of version 4.18.0. If you wish to catch potential reference resolution errors, directly catch referencing.exceptions.Unresolvable.
    from jsonschema.exceptions import RefResolutionError, ValidationError  # noqa

framework/wazuh/core/utils.py:2405
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
  /root/wazuh-4.14.4/framework/wazuh/core/utils.py:2405: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
    return datetime.utcnow().replace(tzinfo=timezone.utc)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 1 passed, 11 warnings in 0.62s ========================

Artifacts Affected

  • wazuh-clusterd (Manager)

Configuration Changes

  • None

Documentation Updates

  • None

Tests Introduced

  • Updated framework/wazuh/core/cluster/tests/test_common.py::test_as_wazuh_object_ok
  • Added coverage for:
    • exposed functions accepted during callable resolution
    • non-exposed functions rejected during callable resolution

Review Checklist

  • Code changes reviewed
  • Relevant evidence provided
  • Tests cover the new functionality
  • Configuration changes documented
  • Developer documentation reflects the changes
  • Meets requirements and/or definition of done
  • No unresolved dependencies with other issues

@vikman90 vikman90 requested review from Copilot, jnasselle and nico-stefani and removed request for jnasselle March 11, 2026 11:16
@vikman90 vikman90 added type/bug Something isn't working module/cluster labels Mar 11, 2026
@vikman90 vikman90 self-assigned this Mar 11, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Cluster DAPI callable deserialization by restricting remote callable resolution to only those functions intended to be remotely executable (i.e., explicitly exposed via @expose_resources), reducing the internal callable surface area.

Changes:

  • Add exposure validation to as_wazuh_object() so only explicitly exposed callables are resolvable.
  • Reject non-exposed callables during DAPI deserialization with a WazuhInternalError.
  • Update test_as_wazuh_object_ok to cover newly blocked callables and one allowed exposed callable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
framework/wazuh/core/cluster/common.py Adds callable exposure checks during JSON deserialization of callables.
framework/wazuh/core/cluster/tests/test_common.py Updates unit test expectations for blocked vs allowed callables.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1852 to +1857
func = getattr(wazuh, funcname)
# Verify that the method has the @expose_resources decorator
if not hasattr(func, '__wrapped__'):
raise exception.WazuhInternalError(1000,
extra_message=f"Method '{funcname}' is not exposed with @expose_resources decorator",
cmd_error=True)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exposure check uses hasattr(func, '__wrapped__') as a proxy for @expose_resources, but __wrapped__ is set by any decorator that uses functools.wraps (e.g. @temporary_cache on wazuh.core.cluster.utils.get_manager_status). This means non-exposed internal functions can still be resolved and executed remotely, defeating the hardening goal. Consider adding an explicit marker in expose_resources (e.g. __wazuh_exposed__ = True or similar) and checking that marker here instead of __wrapped__ (and for methods, check the underlying function via getattr(func, '__func__', func)).

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This. Any wrapped function will be allowed, and there's no way to get the decorator's name. Also, in terms of injecting a DAPI call, we can modify the serialized data to avoid this check

Comment on lines +1879 to +1884
# Verify that the function has the @expose_resources decorator
# The decorator uses functools.wraps which sets the __wrapped__ attribute
if not hasattr(func, '__wrapped__'):
raise exception.WazuhInternalError(1000,
extra_message=f"Function '{funcname}' from module '{module_path}' is not exposed with @expose_resources decorator",
cmd_error=True)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern in the function/static-method path: using __wrapped__ does not prove the callable was exposed via @expose_resources (any @wraps-based decorator will pass), so non-exposed callables from allowed packages can still be deserialized. Switch to an explicit expose_resources marker attribute (and optionally verify callable(func) as well) to ensure only intentionally exposed endpoints are resolvable.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Comment on lines +1799 to +1805
# Test the first condition - non-exposed callables must be blocked (no @expose_resources)
with pytest.raises(exception.WazuhInternalError) as err:
cluster_common.as_wazuh_object({"__callable__": {"__name__": "check_user_master",
"__module__": "api.authentication",
"__qualname__": "check_user_master",
"__type__": "function"}})
assert "is not exposed with @expose_resources decorator" in str(err.value)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated test covers an undecorated function being rejected, but it doesn’t cover the bypass where a non-@expose_resources function still has __wrapped__ due to another @wraps-based decorator (e.g. wazuh.core.cluster.utils.get_manager_status is decorated with @temporary_cache). Adding a regression case for that scenario would help ensure the deserializer only allows @expose_resources callables.

Copilot uses AI. Check for mistakes.
Copy link
Member

@jnasselle jnasselle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GJ @vikman90 , but unfortunately, any wrapped function will be allowed, and also the serialized call can be modified to bypass it by simply modifying the __wrapped__ attribute.

On the other hand, there are (at least one found) methods that are not decorated with expose_resource but still need to be distributed i.e api.authentication.get_security_conf is distributed as part of generate_token during login_user and run_as_login API controllers' functions

IMHO, an exhaustive list of allowed methods is the painful way to solve this. The challenge is to provide a maintainable way to achieve this, but we can start with a hand-made list and keep this tech debt for the moment

@nico-stefani nico-stefani force-pushed the fix/4169-cluster-dapi-filter-callables branch from 84e624e to 62697af Compare March 11, 2026 22:45
vikman90 and others added 6 commits March 12, 2026 08:14
Add validation to ensure only functions with @expose_resources decorator
can be deserialized and executed from network requests. Functions without
the decorator are rejected with WazuhInternalError.
Update test_as_wazuh_object_ok to reflect new behavior where functions
without @expose_resources decorator are properly rejected. Add test case
for get_users which has the required decorator.
@vikman90 vikman90 changed the base branch from 4.14.5 to 4.14.4 March 12, 2026 07:14
@vikman90 vikman90 force-pushed the fix/4169-cluster-dapi-filter-callables branch from 62697af to 93023ba Compare March 12, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module/cluster type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants