Skip to main content

stegdetect

baseline

A Unicode carrier pre-filter for text sent to a language model.

stegdetect inspects Unicode carriers, not instruction meaning. It runs locally with no GPU, model weights, or network calls. The tests exercise specific carriers and legitimate samples; they do not establish prompt-injection prevention or accuracy across all languages and models.

How it works

untrusted text -> analyze() -> { verdict, findings, sanitized } -> LLM
  1. Detect invisible and deceptive Unicode: zero-width characters, bidi overrides, homoglyph (mixed-script) substitution, Unicode tag characters, suspicious whitespace.
  2. Verdict: clean, suspicious, or malicious, with per-finding severity, offsets, and codepoints.
  3. Canonicalize: normalize with NFKC, strip configured invisible carriers, map configured confusables to ASCII, and replace configured unusual spaces. Sanitized text can still contain plain-text injections.

Detection runs on the original text; canonicalization never destroys evidence.

Canonicalization also runs on clean-verdict text. For example, clean Chinese fullwidth punctuation and clean Russian/English code-switching can change in the forwarded copy (pinned in tests/test_fuzz.py). This work preserves that policy; clean does not mean the text is unchanged.

Additive policy API (preview)

inspect_text() separates Unicode evidence from a named decision policy. The default Policy.BALANCED preserves every allowed input exactly. A review, block, invalid input, or scan error has no forwarding candidate.

from stegdetect import inspect_text

inspection = inspect_text(untrusted_text)
if inspection.status != "complete" or inspection.action != "allow":
    hold_for_review_or_block(inspection.as_dict())
else:
    forward_to_llm(inspection.candidate_text)

This is a fixed preview policy, not a validated prompt-injection firewall. The 30 project-authored development fixtures include two benign examples held for review and three constructed attacks allowed by the balanced policy. They cannot establish a deployment false-positive or false-negative rate. See the policy table and limitations and the versioned JSON schema. The committed v2 schema remains available for consumers of the earlier report format; P2 reports identify themselves as v3 because their fields and statuses changed. The legacy analyze() and CLI behavior below remain unchanged.

The additive API accepts trusted Limits and has a bounded UTF-8 entry point. Defaults are 1,048,576 codepoints, 4 MiB of input bytes, 256 retained findings, and 16 MiB of compact ASCII JSON. Truncated finding detail retains exact whole-document counts; limit/error results expose no forwarding candidate. report.to_json() enforces the output cap. The new CLI mode reads file/stdin bytes under the byte cap:

stegdetect --inspect -f retrieved_document.txt

This mode exits 0 for allow, 3 for review/block, and 4 for invalid/limit/error. The legacy CLI modes below retain their existing exits and are not resource-bounded. See the reviewer methodology for measurement and validation.

For legitimate emoji sequences, joining controls, and simple RTL isolates, opt in with inspect_text(text, policy=Policy.CONTEXTUAL) or stegdetect --inspect --contextual. This keeps all original findings and uses schema v4; the default policy and schema v3 stay unchanged. The Unicode context contract states exact recognition boundaries and measured development-set outcomes. For scripts and applications, see CLI batches, explanations, and tested RAG/tool-result gates.

For a static local inspection page, run stegdetect-view -f retrieved_document.txt -o evidence.html. It displays escaped original codepoints and offsets, policy reasons, and clearly labeled preserve/legacy transformation previews. Review and block still produce a page with no forwarding candidate. See the viewer guide.

Independent user tasks and consented shadow pilots have not run. The current P6 status and protocol separate automated rehearsal from real usefulness evidence. A lexical scorer remains a separately gated experiment, outside the Unicode detector and its current release path.

Run python scripts/validate_release.py --profile bounded to verify the full suite, installed wheel, and both source-bound resource artifacts. The measured Windows results are in P2 default-path and P3 context observations; they are not cross-platform latency or memory guarantees.

Install

pip install .

Use

# Scan text, print JSON report
stegdetect "some untrusted input"
# Scan a file
stegdetect -f retrieved_document.txt
# Pipe
curl -s https://example.com/page | stegdetect -
# Just get the sanitized text
stegdetect -f input.txt --sanitize
from stegdetect import analyze

report = analyze(untrusted_text)
if report.verdict == "malicious":
    log(report.as_dict())
    raise BlockedError("steganographic carrier detected")
forward_to_llm(report.sanitized)

Exit code is 0 for clean, 2 otherwise.

What it catches

Carrier Example Severity
Zero-width cluster (bit-encoded payload) doc\u200b\u200c\u200b... high
Bidi override (display order lies) \u202e ... \u202c high
Homoglyph substitution Cyrillic а for Latin a high
Unicode tag characters U+E0000 block high
Invisible formatting soft hyphen, Hangul fillers medium
Non-standard whitespace NBSP, thin space, em space low

What it does not catch (yet)

Plain-text injections and rendering tricks without Unicode carriers remain out of scope. The two boundary tests in tests/test_historical.py deliberately return clean; see docs/HISTORICAL_ATTACKS.md. There is no semantic layer two. Acrostics, synonym substitution, and paraphrase encoding are not detected.

Test

pip install -e ".[dev]"
pytest

P0 adds an executable compatibility baseline:

python scripts/validate_release.py --profile baseline

It checks the full suite, development fixture outcomes, original test-file hashes, five broken-input controls, and an installed wheel outside the checkout. See the detection contract, generator map, and claims ledger. The 30 P0 examples are project-authored development fixtures, with zero independent sources or holdout examples. Their counts are not a general false-positive or false-negative rate. The CI matrix is configured to run the baseline on Python 3.9–3.13 on Linux and Windows plus macOS 3.13; cross-platform support requires actual green CI runs.

Test layout:

  • tests/test_detector.py — the original rule/generator pairs.
  • tests/test_adversarial.py — one test per attack shape, including the deliberate boundaries (sub-threshold zero-width clusters, below-density homoglyphs) where the detector stays silent to avoid false positives.
  • tests/test_false_positives.py — legitimate multilingual, emoji, math, and code samples that must stay clean.
  • tests/test_canonicalize.py — canonicalizer properties: idempotent, offsets index into the original text, NFKC folding.
  • tests/test_cli.py — exit codes and JSON report shape.
  • tests/test_historical.py — reconstructions of documented attacks (Trojan Source, ASCII smuggling, Llama Firewall evasion) plus the two deliberate out-of-scope boundary tests. Full 1-to-1 mapping in docs/HISTORICAL_ATTACKS.md.
  • tests/test_fuzz.py — reproducible Hypothesis properties over all four existing generators, arbitrary Python strings including lone surrogates, finding offsets, normalization equivalence, and deliberate density evasion.
  • tests/test_robustness.py — isolated processes scan eight pathological workloads at 262,144 and 1,048,576 characters, with 30-second timeouts and a loose scaling regression guard.

Measured robustness

Priority 1 (fuzzing/robustness) exposed quadratic ordering in standard NFKC on descending combining-mark runs. The canonicalizer now decomposes each codepoint, orders marks with stable combining-class buckets, then composes already ordered text. Tests compare its output with standard NFKC for every single Python codepoint and bounded generated strings, including composition and compatibility-decomposition cases.

See the measured results and limits for timings and finite FP/FN counts. These are measurements on one machine, not a general no-hang guarantee, memory limit, or proof of worst-case linear runtime. The API tests accept lone-surrogate Python strings; this does not establish that arbitrary surrogates can be written to UTF-8 CLI streams.

Reproduce the benchmark and corpus measurements after installing dev extras:

python benchmarks/robustness.py --output docs/robustness-results.json
python benchmarks/corpus.py --output docs/robustness-corpus.json

Every detection rule has a paired attack generator in src/stegdetect/samples.py. If you add a rule, add a generator.

License

MIT

Metadata

Release files for stegdetect 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stegdetect 0.1.0
File Size Uploaded Provenance Actions
stegdetect-0.1.0.tar.gz 58.4 kB Without provenance Details for stegdetect-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for stegdetect 0.1.0
File Interpreter ABI Platform Actions
stegdetect-0.1.0-py3-none-any.whl Python 3 none any Details for stegdetect-0.1.0-py3-none-any.whl

Total release size: 95.6 kB

Release files / stegdetect-0.1.0.tar.gz

Download URL stegdetect-0.1.0.tar.gz
Size 58.4 kB
Tags Source
SHA-256 checksum
How to use checksums
1f3d9f737229ebb737c6b4b50a450d87b846a559cae0c51142ebe4b2efa9b519
BLAKE2b-256 checksum
How to use checksums
663ab92c0a53f8f5de20418670218379045fe2aa71d3c9af0460582c34f2e89c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / stegdetect-0.1.0-py3-none-any.whl

Download URL stegdetect-0.1.0-py3-none-any.whl
Size 37.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7ce5839a6e5d9830284d19acfae35fa8dadf63a082ab2c0bc9712809eafab8e2
BLAKE2b-256 checksum
How to use checksums
28625325d06cd4645f969ea334be7dadca9e0e0c50610eb27493cd0aa9af4e4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for stegdetect 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page