# How book-to-skill Ensures Security Against Untrusted Documents

> Discover how book-to-skill secures against untrusted documents using defense-in-depth: Unicode sanitization, directory hardening, file-type validation, and controlled error handling.

- Repository: [Virgilio Junior/book-to-skill](https://github.com/virgiliojr94/book-to-skill)
- Tags: security
- Published: 2026-08-26

---

**book-to-skill treats every input file as potentially hostile by implementing defense-in-depth security including invisible Unicode sanitization, symlink-resistant directory hardening, strict file-type validation, and controlled error handling.**

book-to-skill is an open-source document extraction pipeline designed to convert e-books and PDFs into structured text for downstream language model consumption. Because it processes content from potentially untrusted sources, the codebase implements rigorous security measures to prevent prompt injection, directory traversal, and data exfiltration attacks while maintaining robust error boundaries.

## Defense-in-Depth Security Architecture

The security model in book-to-skill operates on the principle that no single check is sufficient for hostile inputs. The pipeline combines four complementary layers: **invisible-character sanitization** to neutralize hidden Unicode payloads, **output-directory hardening** to prevent filesystem-based attacks, **file-type validation** with magic sniffing to block malformed archives, and **safety-first error handling** to ensure exceptions never leak internal state or crash the process.

## Invisible-Character Sanitization to Prevent Prompt Injection

Malicious actors often embed zero-width spaces, bidirectional overrides, and other invisible Unicode code points into documents to create "prompt injection" vectors—instructions that humans cannot see but language models will process. In [`book_to_skill/sanitize.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/sanitize.py), the module defines comprehensive sets of dangerous code points including zero-width spaces (U+200B), bidirectional controls, invisible letters, annotation format controls, tag blocks, variation selectors, and musical format characters.

The function `sanitize_extracted_text()` (lines [39‑50](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/sanitize.py#L39-L50)) processes every extracted string through `is_invisible_codepoint()` to strip these characters before downstream processing. If sanitization removes any characters, the system emits a security warning via the logging infrastructure in [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) (lines [98‑102](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L98-L102)), alerting operators to potential tampering attempts.

```python
from book_to_skill.sanitize import sanitize_extracted_text

raw = "Hello\u200BWorld"               # ZERO-WIDTH SPACE (U+200B) inserted

clean, removed = sanitize_extracted_text(raw)
print(clean)    # → "HelloWorld"

print(removed)  # → 1

```

## Output Directory Hardening Against Directory Traversal

To prevent symlink attacks and unauthorized access, `prepare_output_dir()` in [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) (lines [35‑58](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L35-L58)) implements strict filesystem controls. The function first verifies that the target path is not a symbolic link using `path.is_symlink()`, aborting immediately with an `ExtractionError` if a symlink is detected.

On POSIX systems, the code validates that the directory is owned by the current user ID (UID) before setting restrictive `0o700` permissions (owner read/write/execute only). If the directory does not exist, it is created atomically with mode `0o700`, ensuring no other user can read extracted content or race-condition the directory creation.

```python
from pathlib import Path
from book_to_skill.utils import prepare_output_dir

work_dir = Path("/tmp/book-to-skill-work")
prepare_output_dir(work_dir)   # creates dir with mode 0o700,

                               # aborts if path is a symlink

```

## File Type Validation and Magic Sniffing

Before parsing, book-to-skill validates that only supported, well-known formats enter the extraction pipeline. In `extract_single_file()` within [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) (lines [29‑61](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L29-L61)), the code checks file extensions against an internal `SUPPORTED_EXTENSIONS` whitelist.

If the extension is unknown or suspicious, the system reads the initial bytes to detect PDF headers (`%PDF`) or ZIP-based e-book structures (EPUB, DOCX). Unsupported or malformed archives trigger a clear `ExtractionError` before any parser consumes the content, preventing format confusion attacks and memory exhaustion from malicious archives.

## Safety-First Error Handling

All extraction branches wrap low-level I/O operations in exception handlers that convert `OSError`, `zipfile.BadZipFile`, and similar errors into controlled `ExtractionError` exceptions (lines [34‑57](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L34-L57)). This pattern prevents stack traces from leaking internal path structures or implementation details while ensuring the main processing loop can log the failure and continue safely with remaining files.

When running via CLI, errors surface as clean warning messages rather than crashes:

```bash

# CLI usage – errors report cleanly, never crash

book-to-skill /tmp/malicious.pdf --mode text

```

If hidden Unicode characters are removed, you will see:

```

[security] removed 3 invisible Unicode code point(s) from malicious.pdf

```

If the work directory is compromised:

```

ERROR: Refusing to use /tmp/book-to-skill-work: it is a symbolic link, not a real directory.

```

## Summary

- **Invisible-character sanitization** in [`book_to_skill/sanitize.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/sanitize.py) strips zero-width spaces, bidirectional controls, and other hidden Unicode payloads that could trigger prompt injection in downstream language models.
- **Output-directory hardening** via `prepare_output_dir()` blocks symlinks, enforces UID ownership, and applies `0o700` permissions to prevent data leakage and directory traversal.
- **File-type validation** with magic sniffing in `extract_single_file()` rejects unsupported formats before parsing, blocking malformed archives and format-confusion attacks.
- **Controlled error handling** converts all low-level exceptions into `ExtractionError` instances, preventing information disclosure and ensuring robust pipeline continuity.

## Frequently Asked Questions

### How does book-to-skill prevent prompt injection attacks hidden in documents?

book-to-skill prevents prompt injection by sanitizing all extracted text through `sanitize_extracted_text()` in [`book_to_skill/sanitize.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/sanitize.py), which removes invisible Unicode code points including zero-width spaces, bidirectional controls, and annotation characters that attackers use to hide malicious instructions from human reviewers while ensuring language models still process them.

### What permissions does book-to-skill set for output directories?

The `prepare_output_dir()` function forces restrictive `0o700` permissions (owner-only read, write, and execute) on all working directories. On POSIX systems, it also verifies that the directory is owned by the current user ID before proceeding, preventing other users from accessing extracted content.

### Can book-to-skill detect and block malicious symlinks?

Yes. The `prepare_output_dir()` function explicitly checks `path.is_symlink()` and aborts with an `ExtractionError` if the target path is a symbolic link. This prevents attackers from redirecting output to sensitive system locations or hijacking the extraction directory to overwrite critical files.

### What happens when book-to-skill encounters an unsupported file format?

In `extract_single_file()`, the system first validates extensions against `SUPPORTED_EXTENSIONS`. If the extension is unknown, it performs magic sniffing on the first bytes to detect PDF or ZIP structures. Any unsupported format triggers an immediate `ExtractionError`, ensuring the file never reaches the parser and protecting against format confusion and archive-bomb attacks.