> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Prompt injection defense and QWED security hardening

> Harden QWED for production with prompt injection defense, secret management, network security, authentication, and OWASP LLM Top 10 compliance.

This guide provides instructions for hardening your QWED deployment for production environments. It covers prompt injection defense, secret management, network security, authentication, and compliance with OWASP LLM Top 10.

<Note>
  Only **QWED v7.x** is currently supported with security patches and updates. Versions 6.x and earlier are end-of-life. If you are running a prior version, upgrade before applying the guidance below.
</Note>

## 1. Secret management

QWED relies on environment variables for sensitive configuration. **Never commit `.env` files to version control.**

### Environment variable injection

For production deployments, inject environment variables using your infrastructure's secret management solution.

#### Docker / Kubernetes

Use Kubernetes Secrets or HashiCorp Vault to inject secrets as environment variables into the container.

```yaml theme={null}
# Example Kubernetes Pod Spec
env:
  - name: OPENAI_API_KEY
    valueFrom:
      secretKeyRef:
        name: qwed-secrets
        key: openai-api-key
  - name: JWT_SECRET_KEY
    valueFrom:
      secretKeyRef:
        name: qwed-secrets
        key: jwt-secret
```

#### HashiCorp Vault integration

If you use Vault, you can use `envconsul` or the Vault Agent Injector to populate environment variables before the application starts.

```bash theme={null}
# Example with envconsul
envconsul -prefix qwed/prod/ qwed-api
```

### Critical secrets

### Required secrets

<Info>Changed in v5.0.0</Info>

`API_KEY_SECRET` is now **mandatory** — the server will not start without it. Generate one with:

```bash theme={null}
python -c "import secrets; print(secrets.token_urlsafe(48))"
```

<Info>Changed in v7.2</Info>

API-key lookup digests are now HMAC-SHA256, keyed by the dedicated `QWED_API_KEY_LOOKUP_SECRET` (required — the server fails at startup if it is missing or equal to `QWED_JWT_SECRET_KEY`). Rotating this secret invalidates all existing API-key digests and requires re-issuing every key, so treat it as long-lived. See [Authentication](/api/authentication#api-key-storage-and-migration-v7-2).

Ensure these secrets are rotated regularly:

* `API_KEY_SECRET` (used for API key hashing — **required**)
* `OPENAI_API_KEY` (and other provider keys)
* `API_KEY_SECRET` (used for signing JWTs — **mandatory**, the server will not start without it)
* `JWT_SECRET_KEY` (used for signing session tokens)
* `DATABASE_URL` (if using an external database)

<Warning>
  As of v5.0.0, `API_KEY_SECRET` no longer has a default value. You must set it explicitly before starting the server. Generate a secure value with:

  ```bash theme={null}
  python -c "import secrets; print(secrets.token_urlsafe(48))"
  ```
</Warning>

***

## 2. Network security

### Firewall rules

Restrict network access to the QWED API server:

* **Public Access**: Allow ports `80` / `443` only via a Load Balancer / WAF.
* **Internal Access**: The QWED application port (default `8000`) should **not** be directly exposed to the internet.
* **Database**: Block all public access to the database port (e.g., `5432`). Only allow connections from the QWED application subnet.

### Rate limiting (production)

QWED includes a default in-memory rate limiter that is thread-safe — all check and record operations are protected by a lock, so it is safe to use with multi-threaded ASGI servers. For production, you should tune these limits and consider a distributed solution.

#### Configuration

You can adjust the rate limits using environment variables:

| Environment Variable | Default | Description |
| - | - | - |
| `RATE_LIMIT_REQUESTS_PER_MINUTE` | `100` | Max requests per minute **per API key**. |
| `RATE_LIMIT_WINDOW_SECONDS` | `60` | Time window in seconds. |
| `QWED_RATE_LIMIT_PER_IP` | `10` | Max requests per minute **per client IP** on anonymous `/auth/*` routes. Must be at least 1. |
| `QWED_AUTH_TRUSTED_PROXIES` | empty | Comma-separated proxy IPs/CIDRs trusted to set `X-Forwarded-For` for per-IP throttling. |

**Example:**

```bash theme={null}
export RATE_LIMIT_REQUESTS_PER_MINUTE=50
export RATE_LIMIT_WINDOW_SECONDS=60
```

#### Thread-safe in-memory limiter

<Info>New in v5.0.0</Info>

The in-memory rate limiter is now thread-safe. All check-and-record operations are protected by a lock, which prevents race conditions when multiple threads handle concurrent requests in a single process. This eliminates a class of bypass where two requests arriving simultaneously could both pass the limit check before either was recorded.

#### Redis backing (recommended)

The default in-memory limiter does not scale across multiple worker processes or replicas. For high-availability deployments, use a Redis-backed rate limiter to ensure consistent enforcement across your cluster.

<Note>
  The Redis-backed rate limiter uses a **fail-closed** policy. If Redis becomes unreachable at runtime, requests are denied rather than allowed through. This prevents a Redis outage from silently disabling rate limiting. An in-memory fallback is only used when Redis is unavailable at initialization time.
</Note>

### CORS configuration

QWED requires you to explicitly configure allowed CORS origins. The `QWED_CORS_ORIGINS` environment variable must be set to a comma-separated list of trusted domains — the server will refuse to start if this variable is empty or unset.

<Info>Changed in v5.0.0</Info>

QWED no longer defaults to `*` for CORS origins. The `QWED_CORS_ORIGINS` environment variable is **required** — the server will refuse to start if it is not set.

Set it to a comma-separated list of trusted origins:

```bash theme={null}
export QWED_CORS_ORIGINS="https://app.yourcompany.com,https://admin.yourcompany.com"
```

<Warning>
  Previous versions defaulted to allowing all origins (`*`) for development convenience. As of v5.0.0, CORS origins must be explicitly configured. If you need to allow all origins during local development, set `QWED_CORS_ORIGINS="*"` — but never use this in production.
</Warning>

When the origin list is set to `*`, the `allow_credentials` CORS header is automatically set to `false` to prevent credential leakage. For specific origin lists, credentials are allowed.
If you were previously relying on the default `*` origin, you must now explicitly set `QWED_CORS_ORIGINS` before upgrading. This is a breaking change in v5.0.0.

When `QWED_CORS_ORIGINS` is set to a wildcard (`*`), credentialed requests (`allow_credentials`) are automatically disabled to comply with the CORS specification.

***

## 3. Authentication hardening

### API key rotation

Regularly rotate API keys to minimize the impact of a potential leak.
QWED provides a built-in rotation mechanism via the `POST /admin/keys/rotate` endpoint. Old keys should be revoked immediately after the rotation window.

### Password policies

If you integrate QWED with a custom user database:

* Enforce a minimum length of 12 characters.
* Require a mix of uppercase, lowercase, numbers, and special characters.
* Use the built-in `bcrypt` hashing provided by QWED's authentication module.

### Session management

Control the lifetime of access tokens to reduce the window of opportunity for session hijacking.

| Environment Variable | Default | Description |
| - | - | - |
| `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` | `60` | Minutes until the access token expires. |

**Recommendation:** Set this to a lower value (e.g., `15` or `30` minutes) and implement refresh tokens if needed.

```bash theme={null}
export JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
```

### All-tenant metrics authorization

<Info>Changed in v7.2</Info>

`GET /metrics` and `GET /metrics/prometheus` expose cross-tenant data: request volumes, latencies, provider usage, and per-tenant breakdowns for every organization. Access is a **platform-operator** capability controlled by an explicit allowlist, not an organization role:

| Environment Variable | Default | Description |
| - | - | - |
| `QWED_METRICS_OPERATOR_USER_IDS` | empty | Comma-separated user IDs permitted to read all-tenant metrics. |

<Warning>
  Fail-closed by design: with `QWED_METRICS_OPERATOR_USER_IDS` unset, `/metrics` and `/metrics/prometheus` deny **every** caller — including organization owners and admins.

  Self-service signup mints `role="owner"` for every new account, so organization roles cannot be trusted as platform-wide authority.

  If your monitoring stack scrapes `/metrics/prometheus`, create a dedicated operator account, mint it an API key, and add its user ID to the allowlist **before deploying**. Prometheus's `http_headers` block takes a static header value — it cannot read QWED's environment variables. On Prometheus 2.55+ you can keep the key out of the config file using file-backed `secrets`/`files` entries under `http_headers`, and have your secret-management pipeline render the key into that file.
</Warning>

```bash theme={null}
export QWED_METRICS_OPERATOR_USER_IDS="1,42"
```

Reading the allowlist is done per request, so granting or rotating operators takes effect immediately without a restart. Per-tenant metrics (`GET /metrics/{organization_id}`) are unchanged and remain scoped to the caller's own organization.

***

## 4. Enhanced prompt injection defense

<Info>New in v4.0.0</Info>

QWED v4.0.0 introduces a multi-layer `EnhancedSecurityGateway` (OWASP LLM01:2025 compliant) that screens all inputs through seven defense layers:

| Layer | Defense | Description |
| - | - | - |
| 1 | Pattern detection | Heuristic matching against 14 known injection patterns |
| 2 | Length limiting | Strict 2,000-character limit (blocks \~70% of injection attacks) |
| 3 | Base64 decoding | Detects and decodes base64-encoded payloads, then scans for injection keywords |
| 4 | Semantic similarity | Uses sequence matching against system prompt (threshold: 0.6) |
| 5 | Keyword detection | 28 high-risk keywords: `disregard`, `override`, `bypass`, `jailbreak`, etc. |
| 6 | Unicode script mixing | Detects Cyrillic/Arabic/Greek characters mixed with Latin (homoglyph attacks) |
| 7 | Zero-width characters | Detects invisible characters (`\u200B`, `\u200C`, `\u200D`, `\uFEFF`) used to hide payloads |

The gateway is enabled by default for all API endpoints. It also includes a block counter for monitoring injection attempt frequency.

### PII redaction

All API error paths now use a centralized `redact_pii()` function that masks email addresses, phone numbers, SSNs, and IP addresses before they reach logs. Stack traces are suppressed in error responses (`exc_info=False`) to prevent data leakage.

### Fail-closed code execution

<Info>New in v4.0.4</Info>

All model-generated Python code — in both the Stats and Consensus engines — runs exclusively inside a Docker sandbox (`SecureCodeExecutor`). The in-process Wasm and restricted execution fallbacks that existed in earlier versions have been permanently disabled.

If the Docker daemon is unreachable, the affected endpoints return HTTP 503 rather than degrading to an insecure execution mode. The executor performs a live health check (`docker.ping()`) on every request to detect runtime failures that would not be caught by a startup-time flag.

This design ensures that a Docker outage is surfaced as a visible service disruption rather than silently downgrading security.

#### Sandbox containment limits

Every sandbox container runs with hard resource and output bounds, and is always removed after execution:

* **Process cap** — `pids_limit: 128` prevents fork bombs from exhausting kernel PIDs, alongside the existing memory and CPU limits.
* **Log rotation** — container logs use the `json-file` driver with `max-size: 10m` and `max-file: 1`, so a log-flooding payload cannot fill the host disk.
* **Guaranteed cleanup** — the container is force-removed in a `finally` block covering the whole post-creation lifecycle, including start failures, with one retry for transient daemon races. A cleanup failure is logged as a warning and never discards a valid verification result.
* **Result size cap** — the sandbox result file is capped at 2 MB, enforced both inside the container during serialization and at host read-back. Result payloads stored in the verification audit log are additionally capped and always remain valid JSON, so the audit integrity verifier can parse capped entries.

#### Fail-closed when `CodeVerifier` is unavailable or failing

<Info>Updated May 2, 2026</Info>

`SecureCodeExecutor` delegates code-safety verification to `qwed_new.core.code_verifier.CodeVerifier`. The executor now blocks execution in two scenarios that previously could downgrade safety:

* **Import failure** — `from qwed_new.core.code_verifier import CodeVerifier` raises `ImportError` (for example, after a partial install or version mismatch).
* **Runtime failure** — `CodeVerifier.verify_code()` raises any unexpected exception (for example, an internal engine error). The exception is caught, sanitized, and logged; it is not surfaced to API callers.

Both cases route through the same `_build_fail_closed_safety_denial()` helper, so the response is deterministic. `SecureCodeExecutor.execute()` returns:

```python theme={null}
success = False
error   = "Code safety validation failed: CodeVerifier unavailable; cannot validate code safety."
result  = None
```

If the basic keyword scan would have flagged the input as dangerous, its reason is appended to the error message as advisory context only — it does not authorize execution:

```text theme={null}
Code safety validation failed: CodeVerifier unavailable; cannot validate code safety. Advisory-only fallback also flagged: dangerous operation
```

The container is never started in any of these cases. To restore execution, fix the underlying import error or runtime failure in `qwed_new.core.code_verifier`. Do not rely on the heuristic keyword scan as a substitute for the structured verifier.

#### Legacy `CodeExecutor` hard-blocked

The legacy `CodeExecutor` class — which previously used raw `exec()` to run model-generated Python — is now permanently hard-blocked. Any call to `CodeExecutor.execute()` raises a `RuntimeError` directing you to use `SecureCodeExecutor` instead.

If you import `CodeExecutor` directly in custom code or tests, you must migrate to `SecureCodeExecutor`:

```python theme={null}
# Before (raises RuntimeError)
from qwed_new.core.code_executor import CodeExecutor
executor = CodeExecutor()
executor.execute(code)  # RuntimeError

# After
from qwed_new.core.secure_code_executor import SecureCodeExecutor
executor = SecureCodeExecutor()
result = executor.execute(code)
```

This change has no effect on the public API — the `/verify/stats` and `/verify/consensus` endpoints already use `SecureCodeExecutor` since v4.0.4. It only affects self-hosted deployments that imported `CodeExecutor` directly.

#### `CodeVerifier` failures block execution

If `SecureCodeExecutor` cannot use `CodeVerifier` (the primary structured safety verifier), it now fails closed and refuses to authorize execution. Earlier builds fell back to a basic keyword scan (`_basic_safety_check()`) as the authorization gate when the verifier was unavailable, which weakened the execution boundary from structured verification to heuristic filtering.

The hardened behavior covers both unavailability and runtime errors:

* **Import failure** — If `CodeVerifier` cannot be imported, the safety check returns `(False, "CodeVerifier unavailable; cannot validate code safety.")`.
* **Runtime failure** — If `CodeVerifier.verify_code()` raises any exception during validation, the executor logs a sanitized error and returns the same deterministic denial through the shared `_build_fail_closed_safety_denial()` helper. The exception message is not echoed to callers.
* **Execution blocked** — `SecureCodeExecutor.execute()` returns `(success=False, error="Code safety validation failed: ...", result=None)` and never reaches `containers.run()`.
* **Advisory-only keyword scan** — The basic keyword scan is retained only as diagnostic context. If it also flags the code, its reason is appended to the error message (`"... Advisory-only fallback also flagged: <reason>"`), but it can never authorize execution on its own.

This guarantees that a missing, broken, or crashing primary verifier surfaces as a blocked execution rather than a silent downgrade to heuristic-only filtering.

### Default-deny for unknown tool approvals

<Info>New in v5.1.0</Info>

The tool approval system now blocks all unknown tools by default, regardless of their heuristic risk score. In previous versions, tools with a risk score below `0.3` were automatically approved even if they were not in the allowlist. As of v5.1.0, any tool that is not explicitly allowlisted is denied unconditionally.

If you manage a custom tool allowlist, ensure all tools your agents use are explicitly registered. Unregistered tools return a blocked response with the message `"Unknown tool '<name>' requires explicit allowlisting"`.

### Safe expression evaluation

All `eval()` calls have been fully eliminated and replaced with a custom AST-walking evaluator. Instead of compiling and executing code, the evaluator parses expressions into an AST and validates every node against a strict allow-list. It then interprets the tree directly — no `eval()` or `compile()` is ever invoked.

The safe evaluator enforces three layers of defense:

1. **AST allow-list** — only permitted node types (`Constant`, `Name`, `BinOp`, `UnaryOp`, `Call`, `Tuple`, `List`, and approved operators) pass validation. Unknown or dangerous nodes are rejected before evaluation.
2. **CodeGuard integration** — when the full `qwed_new` package is available, expressions are additionally screened by `CodeGuard` before execution.
3. **Restricted namespace** — the evaluator resolves symbols only from an explicit namespace. For SymPy paths, only whitelisted functions (e.g., `sqrt`, `sin`, `cos`, `log`, `Rational`, `pi`, `E`) are available. For Z3 paths, only `And`, `Or`, `Not`, `Implies`, `If`, `Int`, `Bool`, and `Real` are permitted.

Additional safeguards:

* Keyword unpacking (`**kwargs` via `None`-keyed keywords) is blocked in all call nodes
* `__` (double underscore) patterns are blocked to prevent attribute access attacks
* SymPy expressions use exact arithmetic (`sympy.Integer`, `sympy.Float`) to avoid floating-point drift during comparison

### Credential store security

The `qwed init` wizard and YAML provider config system write `.env` files with strict security guarantees:

* `.env` files written with `0600` permissions (owner-only on Unix)
* Atomic writes via `tempfile` + `os.replace` to prevent partial writes
* Symlink attack prevention (`O_NOFOLLOW` on Unix)
* Automatic `.gitignore` verification ensures `.env` is excluded from version control
* API keys are never printed in full — only the first 8 characters are shown in logs

***

## 5. OWASP LLM Top 10 compliance

QWED implements technical defenses for the [OWASP LLM Top 10](https://owasp.org/www-project-top-10-for-large-language-model-applications/). However, security is a shared responsibility.

### Your responsibilities (user/deployment)

While QWED handles internal verification, you must implement the following "Human in the loop" and deployment safeguards:

#### LLM01: Prompt injection and LLM02: Insecure output handling

* **Human in the loop**: For critical actions (e.g., financial transactions, code deployment), do not rely solely on QWED's verification. Implement a manual approval step.
* **Output monitoring**: Log and randomly audit verified outputs to ensure the verification engine itself hasn't been bypassed.

#### LLM05: Supply-chain vulnerabilities

* **Startup environment integrity**: QWED enforces environment integrity at startup by verifying all Python `.pth` startup hook files against a built-in allowlist. If any unrecognized `.pth` file is found, the server refuses to start. You can extend the allowlist for custom deployments using the `QWED_ALLOWED_STARTUP_PTH_FILES` environment variable (comma-separated list of filenames).

  ```bash theme={null}
  export QWED_ALLOWED_STARTUP_PTH_FILES="my_custom_plugin.pth,internal_tool.pth"
  ```

  If you need to bypass the environment integrity check entirely (for example, in development environments with non-standard `.pth` files), set `QWED_SKIP_ENV_INTEGRITY_CHECK=true`. The server will log a warning when this bypass is active.

  ```bash theme={null}
  # Development only — do not use in production
  export QWED_SKIP_ENV_INTEGRITY_CHECK=true
  ```

* **Startup hook detection**: Use [`StartupHookGuard`](/sdks/guards#startuphookguard) to scan for malicious `.pth` files in Python `site-packages` before your application starts. This defends against supply chain attacks that inject code-execution hooks via compromised PyPI packages.

* **Network Isolation**: Run the QWED backend in a VPC without direct outbound internet access, except to specific LLM provider APIs (allowlist).

* **Dependency Scanning**: Regularly scan your deployment container for vulnerabilities in system packages.

#### LLM06: Sensitive information disclosure

* **Data minimization:** Do not send PII (Personally Identifiable Information) to QWED unless required. Mask or redact sensitive data *before* it reaches the API.

For related deployment controls, see [Architecture overview](/architecture), [SDK guards](/sdks/guards), and [Troubleshooting guide](/troubleshooting).

***

## 6. Secret-scanning webhook intake

<Info>New in v7.3.0</Info>

The `POST /webhooks/secret-scanning` endpoint accepts signed reports of leaked QWED API keys from GitHub secret scanning and compatible partner scanners. Because the endpoint must be reachable by external scanners with no QWED credentials, it is unauthenticated at the transport layer — the ECDSA signature over the raw request body is the only credential, and the intake is fail-closed at every step.

### Trust model

* **Raw-body signature verification.** The signature is verified over the exact request bytes, in the exact order they arrived, before any JSON parsing, logging, or database work. Reformatting the payload would break the signature, so QWED never re-serializes the body.
* **P-256 pinning at key-load.** Signing keys fetched from `https://api.github.com/meta/public_keys/secret_scanning` are pinned to `secp256r1` (`ECDSA-NIST-P256V1-SHA256`) when the PEM is loaded, not at verify time. A future keys-list entry on another curve is dropped at load, never silently trusted.
* **6h key cache with one-shot rotation refetch.** Signing keys are cached in-process for 6 hours. A conditional (`ETag` / `If-None-Match`) refetch keeps a stable key list quiet. If verification fails against an unknown `key_identifier`, QWED refetches the list exactly once (throttled to at most one forced refetch per 60 seconds after a success, 10 seconds after a failure) and retries; anything still unknown fails closed.
* **Timing-safe verdict comparison.** Signature success and failure paths run through `hmac.compare_digest` on a fixed-shape verdict so the two branches are not distinguishable by timing. Every failure — missing headers, malformed base64, unknown key, off-spec curve, bad signature — returns the same generic 401/403 detail.
* **Tokens never logged.** The receipt log line records the match count and the per-source breakdown only. Token values never appear in logs, error messages, or metrics.
* **Fail-closed status separation.** `401` is missing headers, `403` is a proven-bad signature against a fresh trust anchor, and `503` is any state where verification was impossible (signing-key service unreachable, cached anchor unusable, rotation retry throttled). The scanner should redeliver on `503`.

### Envelope limits

Bounded work is enforced before and after the crypto:

* Request body capped at 1 MiB, checked incrementally on read (an over-cap body is rejected the instant the cap is exceeded — the endpoint never buffers an unbounded unsigned body).
* Matches per request capped at 500.
* Individual `token` field capped at 512 characters.
* Signature header capped at 1024 characters before base64 decode.

### Configuration

The endpoint works with no configuration — QWED fetches GitHub's public keys anonymously. Two optional environment variables raise the rate limit on the outbound keys request by attaching a bearer token:

| Environment Variable | Default | Description |
| - | - | - |
| `GITHUB_KEYS_TOKEN` | empty | Personal access token used for the outbound `GET /meta/public_keys/secret_scanning` request. Not required, and never used to authenticate the incoming webhook. |
| `GITHUB_TOKEN` | empty | Fallback used if `GITHUB_KEYS_TOKEN` is not set. |

Neither token grants any authority on the incoming request; only the ECDSA signature does.

### Automatic revocation of leaked keys

After a report passes signature verification, QWED returns `200` to the scanner and processes the verified matches in the background. For each reported token, QWED computes the API-key lookup digest and acts on the result:

| Lookup result | What QWED does |
| - | - |
| Active key found | Revokes the key immediately: sets `is_active=False` and `revoked_at` to the current UTC time. Writes a `SECRET_LEAK_AUTO_REVOKED` security event (severity `high`) and emails the key owner. |
| Key already revoked | No-op. The key is not revoked again and the owner is not emailed again. |
| No matching key | No action. The match is counted as a candidate false positive. |

Handling is idempotent. If the same leak is redelivered, or two deliveries race, exactly one revokes the key and sends the email. The other is treated as already revoked. Scanners report only `qwed_live_` keys — the [API key format spec](/api/authentication#api-key-format) excludes `qwed_test_` keys from detection — so test keys never reach revocation. The revocation sink itself does not filter by prefix.

Revoked keys stop authenticating right away. The owner must issue a replacement key and rotate every service that used the old one.

**Audit trail.** The `SECRET_LEAK_AUTO_REVOKED` security event records the key's `key_preview`, the scanner `source`, and the reported URL. The plaintext token is never written to the database, logs, or email. If the scanner echoes the token inside the URL or other metadata, QWED redacts it to `[REDACTED]` first.

**Owner notification.** QWED sends the email to the user who owns the key. If that user is inactive or has no email address, QWED falls back to the first active `owner` or `admin` of the key's organization. The email subject is `[QWED Security] QWED API key auto-revoked after public exposure`. The body includes the key preview, key name, where the key was found, and the report source. Notification uses the SMTP settings from `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, and `SMTP_PASSWORD`, with a 10-second timeout on every SMTP phase. Owner emails are never throttled, because each revocation is a distinct event.

<Warning>
  Revocation is committed before the notification is attempted. If SMTP is not configured, no owner can be resolved, or delivery fails or times out, QWED logs the failure and the key stays revoked. A notification failure never rolls back a revocation.
</Warning>

Each revocation is committed separately, so one failing row cannot undo revocations for other keys in the same batch. If any row fails, QWED still processes the remaining matches, then logs the batch as failed.

### False-positive feedback reporter

QWED can report a label for each verified match back to the scanning service. Scanners use these labels to improve detection quality over time. The reporter is opt-in and disabled by default.

A match is labeled `true_positive` when its lookup digest matches a real API key (active or revoked), and `false_positive` when nothing matches. QWED computes the labels with a single read-only lookup before revocation. It sends them after the revocation step, whether that step succeeded or failed. The feedback request never delays revocation. It uses a 5-second timeout, and delivery failures are logged and dropped.

By default, each item carries the SHA-256 hash of the token, never the plaintext:

```json theme={null}
[
  {
    "token_type": "qwed_live_api_key",
    "label": "true_positive",
    "token_hash": "<sha256 hex digest of the leaked token>"
  }
]
```

Each item contains either `token_hash` or `token_raw`, never both.

| Environment Variable | Default | Description |
| - | - | - |
| `QWED_LEAK_FEEDBACK_ENABLED` | `false` | Set to `true` to turn on the feedback reporter. |
| `QWED_LEAK_FEEDBACK_URL` | empty | Endpoint that receives the labeled matches as a JSON `POST`. If the reporter is enabled but this is unset, QWED logs a warning and drops the batch. |
| `QWED_LEAK_FEEDBACK_SEND_RAW` | `false` | Set to `true` to send the plaintext token in `token_raw` instead of `token_hash`. Only takes effect with an `https://` feedback URL. Over any other scheme, QWED drops the batch. |

```bash theme={null}
QWED_LEAK_FEEDBACK_ENABLED=true
QWED_LEAK_FEEDBACK_URL=https://scanner.example.com/feedback
# Leave unset to send SHA-256 hashes only (recommended)
# QWED_LEAK_FEEDBACK_SEND_RAW=true
```

<Note>
  Raw mode transmits live secrets back out of your deployment. Enable `QWED_LEAK_FEEDBACK_SEND_RAW` only when the receiving scanner requires plaintext tokens. In raw mode, QWED emits no feedback log lines, so delivery results are not logged.
</Note>

See the [`POST /webhooks/secret-scanning`](/api/endpoints#post-webhooks-secret-scanning) reference for the request/response contract and the full status-code table.

***

## 7. Reporting a vulnerability

If you discover a security vulnerability in QWED, do **not** report it through public GitHub issues, pull requests, or discussions.

Instead, report it privately via email to **[rahul@qwedai.com](mailto:rahul@qwedai.com)**. If GitHub private vulnerability reporting is enabled for the repository, you may use that channel as well.

Include as much detail as possible:

* Steps to reproduce the issue
* Affected version(s)
* Relevant code, configuration, logs, or screenshots
* Proof-of-concept or exploit details, if available
* The potential impact on confidentiality, integrity, or availability

### Response timeline

* Your report will be acknowledged within **24 hours**
* The team will triage and validate the report as quickly as possible
* You will be kept informed of progress during investigation and remediation
* Disclosure timing will be coordinated with you when appropriate

### Coordinated disclosure

Please give the maintainers a reasonable amount of time to investigate and remediate the issue before making any public disclosure. You should:

* Avoid publicly disclosing the issue until a fix or mitigation is available
* Make a good-faith effort to avoid privacy violations, data destruction, or service disruption
* Avoid accessing, modifying, or exfiltrating data beyond what is necessary to demonstrate the issue

### Security issue vs. bug

* **Security issue** — A vulnerability that compromises the confidentiality, integrity, or availability of the system, such as code execution, injection, auth bypass, privilege escalation, sensitive data exposure, sandbox escape, or fail-open security behavior. Report these privately as described above.
* **Bug** — A functional defect or unexpected behavior that does not have security implications, such as a UI issue, incorrect calculation, documentation problem, or non-exploitable crash. Report these via the [GitHub Issue Tracker](https://github.com/QWED-AI/qwed-verification/issues).


## Related topics

- [QWED vs RAG for LLM verification](/advanced/qwed-vs-rag.md)
- [QWED vs Guardrails, RAG, and RLHF for LLM verification](/advanced/comparison.md)
- [LLM verification with formal methods](/advanced/llm-verification.md)
- [QWED protocol specification](/specs/qwed-spec.md)
- [QWED: deterministic verification for LLMs and AI agents](/intro.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.