Skip to content

fix: canonicalize include paths to prevent directory traversal (CWE-22)#45

Open
0xDanielSec wants to merge 1 commit intogoogle:masterfrom
0xDanielSec:fix/path-traversal-include-resolution
Open

fix: canonicalize include paths to prevent directory traversal (CWE-22)#45
0xDanielSec wants to merge 1 commit intogoogle:masterfrom
0xDanielSec:fix/path-traversal-include-resolution

Conversation

@0xDanielSec
Copy link
Copy Markdown

Bug

includes_resolve() in src/includes.c passes the raw output of
path_join() directly to fopen() without validating that the
resulting path stays within the intended search directory.
path_join() is a simple string concatenation — it does not strip or
resolve ../ sequences. An attacker who controls the content of a
kafel policy file can embed a path traversal sequence in an #include
directive to open arbitrary files readable by the process.

Reproduction

Given a search path /var/lib/policies and a policy file containing:
#include "../../../etc/shadow"
path_join() produces /var/lib/policies/../../../etc/shadow. The OS
resolves this to /etc/shadow at fopen() time. If the file is
readable by the process, the lexer opens and begins parsing it. While
the content is not echoed back verbatim, the attack enables:

  • Filesystem probing — distinguishing "file not found" from a parse
    error confirms whether arbitrary paths exist.
  • Partial content disclosure — parse error messages include the
    first unexpected token and its line/column, leaking content fragments.
  • Denial of service — including /dev/urandom or a named pipe
    causes the lexer to block or spin.
  • Symlink traversal — a symlink inside the search directory that
    points outside it is also accepted by the unpatched code.
    The MAX_INCLUDE_DEPTH guard (16 levels) does not mitigate this; it
    only prevents unbounded recursion.
    Affected versions: all versions since include support was added
    (commit 8724cf5, Sep 2018).

Fix

After path_join(), call realpath(path, NULL) to obtain the
canonical absolute path with all ../, ./, and symlink components
resolved. Then call realpath() on the search directory itself and
verify the canonical file path has the canonical search path as a
proper prefix (guarding the boundary with a / or \0 check to
prevent /foo/bar from matching /foo/barbaz). Reject the include
and continue to the next search path if the check fails.
If realpath() returns NULL (file does not exist, or a path
component is inaccessible), the search path is skipped — identical
behavior to a failed fopen() in the original code.
The realpath() return value is malloc-allocated; ownership is
transferred to the used_paths list, which includes_ctxt_clean()
already frees, so there is no change to memory ownership semantics.

Changed files

  • src/includes.c — includes_resolve() only. No API changes, no
    struct changes, no new dependencies (realpath is POSIX.1-2008;
    stdlib.h and string.h were already included).

Security impact

Exploitability requires an attacker to control the content of a policy
file passed to kafel_compile() or kafel_compile_file(). The typical
deployment (policy authored by the application developer at build time)
is not directly affected. However, any application that compiles
user-supplied or externally-fetched kafel policies — including
policy-as-a-service tools, container runtimes, or test harnesses — is
at risk.

includes_resolve() previously passed the raw output of path_join()
directly to fopen(), allowing filenames containing ../ sequences to
escape the registered search directories.

Call realpath() on the joined path before opening it, then verify the
canonical result shares a prefix with the canonical search directory.
Reject the include if the resolved path falls outside that boundary.

Fixes CWE-22 (Path Traversal).
@google-cla
Copy link
Copy Markdown

google-cla bot commented Apr 10, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@robertswiecki
Copy link
Copy Markdown
Collaborator

Thank you 0xDanielSec.

I do not think it's within our threat model - ie. this "attack" doesn't cross any privilege boundaries.

@happyCoder92 wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants