{"record":{"id":"ab140f720243646f","repo":"nodejs/node","slug":"r-is-not-safely-callable","errorCode":null,"errorMessage":"%r is not safely callable","messagePattern":"%r is not safely callable","errorType":"exception","errorClass":"SecurityError","httpStatus":null,"severity":"error","filePath":"tools/inspector_protocol/jinja2/sandbox.py","lineNumber":426,"sourceCode":"        \"\"\"\n        if isinstance(s, Markup):\n            formatter = SandboxedEscapeFormatter(self, s.escape)\n        else:\n            formatter = SandboxedFormatter(self)\n        kwargs = _MagicFormatMapping(args, kwargs)\n        rv = formatter.vformat(s, args, kwargs)\n        return type(s)(rv)\n\n    def call(__self, __context, __obj, *args, **kwargs):\n        \"\"\"Call an object from sandboxed code.\"\"\"\n        fmt = inspect_format_method(__obj)\n        if fmt is not None:\n            return __self.format_string(fmt, args, kwargs)\n\n        # the double prefixes are to avoid double keyword argument\n        # errors when proxying the call.\n        if not __self.is_safe_callable(__obj):\n            raise SecurityError('%r is not safely callable' % (__obj,))\n        return __context.call(__obj, *args, **kwargs)\n\n\nclass ImmutableSandboxedEnvironment(SandboxedEnvironment):\n    \"\"\"Works exactly like the regular `SandboxedEnvironment` but does not\n    permit modifications on the builtin mutable objects `list`, `set`, and\n    `dict` by using the :func:`modifies_known_mutable` function.\n    \"\"\"\n\n    def is_safe_attribute(self, obj, attr, value):\n        if not SandboxedEnvironment.is_safe_attribute(self, obj, attr, value):\n            return False\n        return not modifies_known_mutable(obj, attr)\n\n\n# This really is not a public API apparenlty.\ntry:\n    from _string import formatter_field_name_split","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/nodejs/node/blob/1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e/tools/inspector_protocol/jinja2/sandbox.py#L408-L444","documentation":"Raised by SandboxedEnvironment.call when the object being invoked from sandboxed template code is not marked as safely callable. is_safe_callable returns True only for callables explicitly approved: marked with @safe or possessing the unsafe_callable attribute unset (default safe), and explicitly excluded when decorated with @unsafe. The SecurityError prevents sandboxed templates from invoking arbitrary Python functions.","triggerScenarios":"Template calls {{ user.delete_account() }} or {{ os.system('rm -rf') }} where the callable was passed into the context but is not approved. Passing a class instance method marked @unsafe into the template context and invoking it. Calling a callable that the sandbox does not recognize as formatted/known-safe.","commonSituations":"Leaking ORM model methods or framework helpers into the template context. Using SandboxedEnvironment for user-uploaded templates and accidentally exposing privileged callables. Marking too many things @unsafe during a security review and breaking legitimate calls.","solutions":["Decorate the genuinely safe callable with @jinja2.sandbox.safe so is_safe_callable returns True.","Do not pass the callable into the context; instead expose a vetted wrapper function that performs the operation in Python.","Refactor the template to call only attributes/values, not arbitrary methods; precompute results in Python.","Audit context objects: replace live ORM/manager objects with plain dicts or data-transfer objects."],"exampleFix":"// before\ndef render(template_str, ctx):\n    return SandboxedEnvironment().from_string(template_str).render(**ctx)\nrender('{{ acct.delete() }}', {'acct': Account(...)})\n// after\nfrom jinja2.sandbox import safe\n@safe\ndef delete_account(acct):\n    acct.delete()\n    return 'deleted'\nrender('{{ delete_account(acct) }}', {'acct': acct, 'delete_account': delete_account})","handlingStrategy":"type-guard","validationCode":"from jinja2.sandbox import SandboxedEnvironment, unsafe\n# whitelist: build the context from a dataclass/dict, never from live model objects\nctx = {'user': {'name': acct.name, 'email': acct.email}}  # plain dict only\nenv = SandboxedEnvironment()\nenv.from_string(tpl).render(**ctx)","typeGuard":"from jinja2.sandbox import safe\nfrom functools import wraps\n\ndef is_safe_callable(obj) -> bool:\n    return callable(obj) and not getattr(obj, 'unsafe_callable', False)\n\n# explicit allow-list decorator\n@safe\ndef approved(fn):\n    return fn","tryCatchPattern":"from jinja2.exceptions import SecurityError\ntry:\n    env.from_string(tpl).render(**ctx)\nexcept SecurityError as e:\n    log.warning('sandbox rejected callable: %s', e)\n    html = render_safe_fallback()","preventionTips":["Pass plain dicts / DTOs into sandboxed templates, never ORM models or framework objects.","Mark callables @jinja2.sandbox.safe only after a deliberate review.","Run user-supplied templates under ImmutableSandboxedEnvironment in addition to the basic sandbox."],"tags":["jinja2","sandbox","security","callable","securityerror"],"backgroundTag":null,"analyzedSha":"1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e","analyzedAt":"2026-08-13T00:53:24.642Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}