{"record":{"id":"d1b3969221022a15","repo":"infiniflow/ragflow","slug":"artifact-symlinks-are-not-allowed-relative-path-d1b396","errorCode":null,"errorMessage":"Artifact symlinks are not allowed: {relative_path}","messagePattern":"Artifact symlinks are not allowed: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/sandbox/providers/ucloud_agent_sandbox.py","lineNumber":426,"sourceCode":"        \"\"\"Collect allowed files from the execution artifact directory.\"\"\"\n        artifacts: list[dict[str, Any]] = []\n        self._collect_artifacts_recursive(sandbox, artifacts_dir, \"\", artifacts, depth=0)\n        return artifacts\n\n    def _collect_artifacts_recursive(self, sandbox, current_dir: str, relative_dir: str, artifacts: list[dict[str, Any]], depth: int) -> None:\n        \"\"\"Traverse artifact directories while enforcing type, size, and depth limits.\"\"\"\n        if depth > MAX_ARTIFACT_DEPTH:\n            raise RuntimeError(f\"Artifact directory nesting exceeds {MAX_ARTIFACT_DEPTH} levels: {relative_dir}\")\n        sdk = _get_ucloud_sandbox_module()\n        try:\n            entries = sandbox.files.list(current_dir, depth=1, request_timeout=self.timeout)\n        except sdk.FileNotFoundException:\n            return\n        for entry in sorted(entries, key=lambda item: item.path):\n            name = posixpath.basename(entry.path)\n            relative_path = posixpath.join(relative_dir, name) if relative_dir else name\n            if entry.symlink_target is not None:\n                raise RuntimeError(f\"Artifact symlinks are not allowed: {relative_path}\")\n            if entry.type == sdk.FileType.DIR:\n                self._collect_artifacts_recursive(sandbox, entry.path, relative_path, artifacts, depth + 1)\n                continue\n            if len(artifacts) >= self.max_artifacts:\n                raise RuntimeError(f\"UCloud Agent Sandbox execution produced more than {self.max_artifacts} artifacts.\")\n            if entry.size > self.max_artifact_bytes:\n                raise RuntimeError(f\"Artifact exceeds {self.max_artifact_bytes} bytes: {relative_path}\")\n            extension = os.path.splitext(name)[1].lower()\n            if extension not in ALLOWED_ARTIFACT_EXTENSIONS:\n                raise RuntimeError(f\"Unsupported artifact type: {relative_path}\")\n            content = bytes(sandbox.files.read(entry.path, format=\"bytes\", request_timeout=self.timeout))\n            artifacts.append(\n                {\n                    \"name\": relative_path,\n                    \"content_b64\": base64.b64encode(content).decode(\"ascii\"),\n                    \"mime_type\": mimetypes.guess_type(name)[0] or \"application/octet-stream\",\n                    \"size\": entry.size,\n                }","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/agent/sandbox/providers/ucloud_agent_sandbox.py#L408-L444","documentation":"Raised during artifact collection when a listed file entry has a non-None `symlink_target`. Symlinks are categorically rejected to prevent exfiltrating files outside the artifacts directory (a link pointing at /etc/passwd or another tenant's data) — a security guard, not a size limit.","triggerScenarios":"Executed code creating `os.symlink('/etc/passwd', 'artifacts/secret')` or symlinking any external path into the artifacts directory; collection then aborts the whole execution result.","commonSituations":"Convenient-looking LLM code that symlinks inputs into the output dir; build scripts whose tooling creates relative symlinks (e.g. node .bin links) inside the artifacts tree; prompt-injection attempts trying to read host files.","solutions":["Copy files into the artifacts directory instead of symlinking (`shutil.copy`, not os.symlink).","Configure build tools to emit plain files or place their symlinked dirs outside artifacts.","Treat this error as a security signal — audit user code that tried to symlink absolute paths.","Do not attempt to bypass by raising MAX depth/size; the check is unconditional by design."],"exampleFix":"# before\nos.symlink(\"/etc/passwd\", \"artifacts/passwd\")  # collection -> RuntimeError\n\n# after\nshutil.copy(\"input.txt\", \"artifacts/input.txt\")","handlingStrategy":"try-catch","validationCode":"# reject symlink creation in code you send to the sandbox\nif \"os.symlink\" in code or \"ln -s\" in code:\n    raise ValueError(\"symlinks are not allowed in artifacts; copy files instead\")","typeGuard":"def is_symlink_artifact_error(exc: RuntimeError) -> bool:\n    return \"symlinks are not allowed\" in str(e if (e := exc) else \"\")","tryCatchPattern":"try:\n    result = provider.execute(inst, code)\nexcept RuntimeError as e:\n    if \"symlinks are not allowed\" in str(e):\n        # security signal: inspect what the code tried to link before retrying\n        audit_and_reject(code)\n    raise","preventionTips":["Use shutil.copy/copyfile for inputs placed in the artifacts directory.","Treat this error as potential prompt-injection/exfiltration when the link target is outside the sandbox workspace.","Lint generated code for os.symlink/ln -s before dispatch in security-sensitive deployments."],"tags":["security","symlink","artifacts","ucloud","sandbox"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}