Tencent/WeKnora · error

invalid source path: %w

Error message

invalid source path: %w

What it means

CopyFile wraps the error from secutils.SafePathUnderBase for the SOURCE path when it does not resolve inside baseDir — the same traversal guard GetFile applies. The copy is refused before reading anything. The wrapped error identifies the path violation.

Source

Thrown at internal/application/service/file/local.go:176

// The destination uses the same layout as SaveFile (baseDir/{tenantID}/{knowledgeID}/{unique}{ext}),
// and the copy is a real byte-for-byte copy (no hardlink) so deleting the source
// never affects it. Returns ErrCrossBackendCopy when srcPath is not a local path.
func (s *localFileService) CopyFile(ctx context.Context,
	srcPath string, tenantID uint64, knowledgeID string,
) (string, error) {
	// Only local paths are accepted. A provider scheme other than local://
	// (e.g. s3://, minio://) means a cross-backend copy, which this service
	// does not support. Legacy bare/absolute paths have no scheme and pass.
	if i := strings.Index(srcPath, "://"); i >= 0 && srcPath[:i+3] != localScheme {
		return "", fmt.Errorf("local file service cannot copy %q: %w", srcPath, ErrCrossBackendCopy)
	}

	// Validate and resolve the source path under baseDir (same guard as GetFile).
	srcCandidate := s.normalizePathForBase(srcPath)
	srcResolved, err := secutils.SafePathUnderBase(s.baseDir, srcCandidate)
	if err != nil {
		logger.Errorf(ctx, "Path traversal denied for CopyFile src: %v", err)
		return "", fmt.Errorf("invalid source path: %w", err)
	}

	// Build destination path with the knowledge-owned layout.
	dir := filepath.Join(s.baseDir, fmt.Sprintf("%d", tenantID), knowledgeID)
	if _, err := secutils.SafePathUnderBase(s.baseDir, dir); err != nil {
		logger.Errorf(ctx, "Path traversal denied for CopyFile dir: %v", err)
		return "", fmt.Errorf("invalid path: %w", err)
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return "", fmt.Errorf("failed to create directory: %w", err)
	}

	ext := filepath.Ext(srcPath)
	filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
	dstPath := filepath.Join(dir, filename)

	src, err := os.Open(srcResolved)
	if err != nil {

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Ensure the source is the relative local:// path originally returned by SaveFile
  2. Re-baseline stored paths after any baseDir change (migration script stripping the old prefix)
  3. Reject '..'/absolute paths at the API layer before invoking CopyFile
  4. Verify LOCAL_STORAGE_BASE_DIR matches the one used when the source was saved

Example fix

// before: legacy absolute path stored in DB
src := record.StoragePath // /var/data/old/uploads/a.pdf
// after: migrate to relative path at read time
src := strings.TrimPrefix(record.StoragePath, oldBaseDir)
copied, err := localSvc.CopyFile(ctx, src, tenantID, knowledgeID)
Defensive patterns

Strategy: validation

Validate before calling

func validSourcePath(p string) bool {
	return p != "" && !filepath.IsAbs(p) && !strings.Contains(p, "..") &&
		(!strings.Contains(p, "://") || strings.HasPrefix(p, "local://"))
}

Try / catch

copied, err := svc.CopyFile(ctx, src, tenantID, kid)
if err != nil {
	if errors.Is(err, secutils.ErrPathTraversal) {
		return fmt.Errorf("bad source path: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling CopyFile with a source path containing '..' escaping baseDir, an absolute path outside baseDir, a bare legacy path that normalizes outside the base, or a path from a differently-configured instance.

Common situations: Legacy DB rows with absolute paths predating a baseDir change; client-supplied source paths passed through unvalidated; baseDir moved (e.g. /var/data -> /data) making old stored paths resolve outside the new base.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/61d4b550db4d7137. Report an issue: GitHub.