infiniflow/ragflow · error · RuntimeError

Artifact symlinks are not allowed: {relative_path}

Error message

Artifact symlinks are not allowed: {relative_path}

What it means

Raised as RuntimeError when artifact collection encounters a symbolic link anywhere under the artifacts directory. Symlinks are categorically rejected (via stat.S_ISLNK on lstat-derived mode) because following them would let sandboxed code exfiltrate arbitrary remote files (e.g. linking 'report.pdf' -> /etc/shadow) into the returned base64 artifacts. This is a deliberate security guard, not a size/format limitation.

Source

Thrown at agent/sandbox/providers/ssh.py:641

        artifacts: list[dict[str, Any]],
    ) -> None:
        try:
            entries = sftp.listdir_attr(current_dir)
        except FileNotFoundError:
            return

        for entry in sorted(entries, key=lambda item: item.filename):
            name = entry.filename
            remote_path = posixpath.join(current_dir, name)
            relative_path = posixpath.join(relative_dir, name) if relative_dir else name
            mode = entry.st_mode
            if mode is None:
                mode = sftp.lstat(remote_path).st_mode
            if mode is None:
                raise RuntimeError(f"Unable to determine artifact entry type: {relative_path}")

            if stat.S_ISLNK(mode):
                raise RuntimeError(f"Artifact symlinks are not allowed: {relative_path}")
            if stat.S_ISDIR(mode):
                self._collect_artifacts_recursive(sftp, remote_path, relative_path, artifacts)
                continue
            if not stat.S_ISREG(mode):
                raise RuntimeError(f"Unsupported artifact entry: {relative_path}")

            if len(artifacts) >= self.max_artifacts:
                raise RuntimeError(f"SSH execution produced more than {self.max_artifacts} artifacts.")

            size = int(entry.st_size or 0)
            if size > self.max_artifact_bytes:
                raise RuntimeError(f"Artifact exceeds {self.max_artifact_bytes} bytes: {relative_path}")

            ext = os.path.splitext(name)[1].lower()
            if ext not in ALLOWED_ARTIFACT_EXTENSIONS:
                raise RuntimeError(f"Unsupported artifact type: {relative_path}")

            with sftp.file(remote_path, "rb") as artifact_file:

View on GitHub (pinned to 554fb1133a)

Solutions

  1. Remove symlinks from sandboxed code before returning: write real files, or copy link targets you legitimately own
  2. Use a fresh instance/workspace per execution so stale links cannot accumulate
  3. If it appeared unexpectedly, audit the executed code — this guard firing is a strong exfiltration signal

Example fix

# before (sandboxed code)
os.symlink('/etc/passwd', 'artifacts/creds.json')

# after (sandboxed code)
with open('artifacts/report.json', 'w') as f:
    f.write(json.dumps({'status': 'ok'}))
Defensive patterns

Strategy: try-catch

Validate before calling

# inside sandboxed code, before returning
import os
for root, _, files in os.walk(artifacts_dir):
    for f in files:
        p = os.path.join(root, f)
        if os.path.islink(p):
            os.remove(p)

Try / catch

try:
    artifacts = provider.collect_artifacts(instance_id, artifacts_dir)
except RuntimeError as e:
    if "symlinks are not allowed" in str(e):
        log.security("sandboxed code attempted symlink exfiltration: %s", e)
        raise

Prevention

When it happens

Trigger: Sandboxed code creating os.symlink('/etc/passwd', 'out/data.json') or ln -s before finishing; artifacts dir containing links left by a prior run; language runtimes that symlink cache/output files into the working directory.

Common situations: Prompt-injected or malicious generated code trying to read remote secrets via the artifact channel; build tools (npm, pip) symlinking into workspace output; shared workspaces reused across executions.

Related errors


AI-assisted analysis of infiniflow/ragflow@554fb1133a (2026-08-15). Data as JSON: /api/errors/c9c9d45782089285. Report an issue: GitHub.