Skip to content

🛡️ feat: Configurable Message PII Filter#13602

Merged
danny-avila merged 8 commits into
devfrom
feat/pii-filter-middleware
Jun 10, 2026
Merged

🛡️ feat: Configurable Message PII Filter#13602
danny-avila merged 8 commits into
devfrom
feat/pii-filter-middleware

Conversation

@dustinhealy

@dustinhealy dustinhealy commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an opt-in PII filter that scans user prompts for credential-shaped patterns and rejects the request with HTTP 400 + the message_filter_pii_block code when one matches. A matched credential never reaches OpenAI moderation, the model, the database, or any other downstream consumer. Three starter patterns ship by default (sk- prefix tokens, Bearer headers, api-key: headers); operators can subset them with starterPatterns: [...] or supply their own regex via customPatterns. Omit the section entirely to leave the filter inert.

The yaml config lives under messageFilter.pii rather than a flat messagePiiFilter, so future safety filters (e.g. messageFilter.toxicity) can plug in alongside without restructuring the schema later.

The same pattern set guards three entry points so a caller cannot bypass the filter by switching protocols:

  • /api/agents/chat — an Express middleware mounted ahead of moderateText.
  • /api/agents/v1/chat/completions — the OpenAI-compatible controller checks request.messages after the agent lookup.
  • /api/agents/v1/responses — the Responses controller checks the converted internal messages just after convertToInternalMessages.

Each entry point returns the 400 in its endpoint-native error shape ({ error, message } for the chat route, the OpenAI/Responses error envelopes for the remote APIs), all carrying the same message_filter_pii_block code so callers can branch on a single code.

Implementation lives in packages/api/src/middleware/messageFilterPii.ts: a WeakMap-memoized compile of starter + custom patterns, plus two consumers — the chat-route createMessageFilterPii middleware factory and a findPiiMatchInMessages helper that handles both content: string and content: ContentPart[] shapes used by the remote APIs. Schema lives in packages/data-provider/src/config.ts (messageFilterSchema = z.object({ pii: messageFilterPiiSchema.optional() })). The route mount is in api/server/routes/agents/chat.js; the controller hooks are in api/server/controllers/agents/openai.js and responses.js.

Change Type

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Testing

Screen.Recording.2026-06-08.at.11.17.01.AM.mov

Test Configuration:

Add messageFilter: { pii: {} } to librechat.yaml (or the example block from librechat.example.yaml for explicit patterns). Run backend + frontend, send chat messages containing credential-shaped text, or call the remote APIs with an agent API key (Authorization: Bearer <key>).

Automated:

  • cd packages/api && npx jest src/middleware/messageFilterPii.spec.ts — 20 unit cases covering middleware behavior, the messages-array helper for both content shapes, starterPatterns: [] semantics, customPatterns matching alongside and in place of starters, the empty-text/no-config pass-throughs, memoization, and the invalid-regex override fallback.
  • cd api && npx jest server/controllers/agents/ — 278 controller tests pass with the new mocks for findPiiMatchInMessages.

Manual end-to-end against a running backend (verified):

  • Plain message via the chat UI goes through normally
  • my key is sk-proj-FAKE1234567890ABCDEF via the chat UI is rejected with 400 + message_filter_pii_block
  • POST /api/agents/v1/chat/completions with a normal user message returns 200 with the agent reply
  • POST /api/agents/v1/chat/completions with sk- in the user content returns 400 in the OpenAI error envelope with code: "message_filter_pii_block"
  • POST /api/agents/v1/responses with normal input returns 200
  • POST /api/agents/v1/responses with sk- in input returns 400 with the same code in the Responses error envelope

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • A pull request for updating the documentation has been submitted (📚 docs: Document messagePiiFilter object LibreChat-AI/librechat.ai#599)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in, config-driven middleware to detect credential-shaped patterns in agent chat input and block requests early (HTTP 400) before moderation/model/persistence, with YAML/schema support for starter + custom regex patterns.

Changes:

  • Introduces createMessagePiiFilter Express middleware with a starter catalog + operator-defined regex patterns.
  • Extends config schema/types to accept messagePiiFilter and threads it into the resolved AppConfig (req.config).
  • Mounts the middleware on the agents chat router ahead of moderateText, and documents usage in librechat.example.yaml.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/data-schemas/src/types/app.ts Adds messagePiiFilter to AppConfig typing.
packages/data-schemas/src/app/service.ts Exposes messagePiiFilter on the resolved AppConfig used by req.config.
packages/data-provider/src/config.ts Adds Zod schema/type for messagePiiFilter and custom regex validation.
packages/api/src/middleware/messagePiiFilter.ts Implements the new PII filter middleware and starter patterns.
packages/api/src/middleware/index.ts Re-exports the new middleware from the API package.
api/server/routes/agents/chat.js Mounts the middleware on agent chat routes before moderateText.
librechat.example.yaml Documents configuration examples for messagePiiFilter.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/api/src/middleware/messagePiiFilter.ts Outdated
Comment thread packages/api/src/middleware/messagePiiFilter.ts Outdated
Adds an opt-in `messagePiiFilter` middleware mounted on the agent
chat route ahead of `moderateText`. When the configured patterns
match the user's input the request is refused with 400, so the
credential never reaches OpenAI moderation, the model, or MongoDB.
Three starter patterns ship by default and operators can subset
them or add their own regex via `customPatterns` in librechat.yaml.
@dustinhealy dustinhealy force-pushed the feat/pii-filter-middleware branch from 31c8348 to 7f638b5 Compare June 8, 2026 18:40
@dustinhealy

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f638b503a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/routes/agents/chat.js Outdated
Memoize the compiled pattern array via a WeakMap keyed by the
messagePiiFilter config object so repeat requests against the same
config skip the per-request RegExp construction. Cache entries are
released automatically when the config object itself rotates.

Adds packages/api/src/middleware/messagePiiFilter.spec.ts covering
the default-starter rejections, the starterPatterns subset and
empty-array semantics, customPatterns matching layered on top of and
in place of the starters, the no-config and empty-text pass-through
paths, and a memoization regression check.
@dustinhealy

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea446b12ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/routes/agents/chat.js Outdated
Comment thread packages/api/src/middleware/messagePiiFilter.ts Outdated
…quest

Admin DB overrides for `messagePiiFilter.customPatterns` reach
`req.config` via `mergeConfigOverrides`, which deep-merges raw
override values without re-running `configSchema`. A typo'd regex
like `(` would slip past the YAML-load validation and throw inside
`new RegExp(...)` during `compile()`, returning 500 for every chat
request until the operator rolled the override back.

Wrapped the per-pattern compile in a try/catch that logs the
invalid pattern id + reason and skips it, so other valid patterns
(starters and other custom entries) keep filtering. Added a
regression test alongside the existing spec.
@dustinhealy

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81b37a1a9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/routes/agents/chat.js Outdated
The chat-route middleware operates on `req.body.text`, but the remote
agent API endpoints (`/api/agents/v1/chat/completions`,
`/api/agents/v1/responses`) accept the same prompt content as a
`messages` array or an `input` field. A caller using their API key
could send a credential-shaped value through either route and bypass
the configured PII filter even though they share the same agent and
model backbone the middleware is meant to guard.

Factored out `findPiiMatchInMessages`, a tolerant walker that handles
both `content: string` and `content: ContentPart[]` user-message
shapes against the same compiled, cached pattern list. Wired it into
the OpenAI-compat controller after agent lookup and into the
Responses controller right after `convertToInternalMessages`. Each
returns the endpoint's native 400 error shape
(`sendErrorResponse` / `sendResponsesErrorResponse`) with the
`message_pii_filter_block` code when a user message matches.
@dustinhealy

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ocks

The OpenAI-compat and Responses controller specs mock `@librechat/api`
with a hand-listed object. The new `findPiiMatchInMessages` export
wired into both controllers in 3ea35af was missing from those
mocks, so the production lookup returned undefined and the controllers
threw at request time under jest. Added the missing entries (default
mock: returns null so the handlers fall through to the existing happy
paths). All 278 agents-controller tests pass locally.
@dustinhealy dustinhealy changed the title 🛡️ feat: Configurable Message PII Filter Middleware 🛡️ feat: Configurable Message PII Filter Jun 8, 2026
@dustinhealy dustinhealy marked this pull request as ready for review June 8, 2026 19:52
Comment thread api/server/controllers/agents/openai.js Outdated
Comment thread api/server/controllers/agents/responses.js Outdated
Comment thread packages/api/src/middleware/index.ts Outdated
…import order

Renames the yaml field `messagePiiFilter` to `messageFilter.pii`, the
module to `messageFilterPii`, the factory to `createMessageFilterPii`,
the type to `MessageFilterPiiConfig`, and the error code to
`message_filter_pii_block`. The wrapper `messageFilter` namespace
gives future safety filters (e.g. `messageFilter.toxicity`) a place
to plug in without restructuring the config later. The
`findPiiMatchInMessages` helper kept its name because it already
describes what it does at the value level.

Also fixes import order Danny flagged on the OpenAI-compatible and
Responses controllers: `findPiiMatchInMessages` was appended at the
bottom of two `require('@librechat/api')` destructures rather than
placed in the length-sorted slot the house style expects.
dustinhealy added a commit to LibreChat-AI/librechat.ai that referenced this pull request Jun 9, 2026
Renames the docs page to match the upstream config rename in
danny-avila/LibreChat#13602: messagePiiFilter is now nested under
messageFilter as `messageFilter.pii`, and the page describes the
namespace so future filter types (e.g. toxicity) have a documented
home. Updates the top-level config.mdx anchor and the
object_structure navigation accordingly. Error code in all examples
is now `message_filter_pii_block`.
dustinhealy added a commit to LibreChat-AI/librechat.ai that referenced this pull request Jun 9, 2026
Renames the docs page to match the upstream config rename in
danny-avila/LibreChat#13602: messagePiiFilter is now nested under
messageFilter as `messageFilter.pii`, and the page describes the
namespace so future filter types (e.g. toxicity) have a documented
home. Updates the top-level config.mdx anchor and the
object_structure navigation accordingly. Error code in all examples
is now `message_filter_pii_block`.
@dustinhealy dustinhealy marked this pull request as draft June 9, 2026 18:10
@dustinhealy dustinhealy marked this pull request as ready for review June 9, 2026 18:14
Reorders the general sub-group inside the `require('@librechat/api')`
destructure shortest to longest so the whole block conforms to the
length-sort rule the file's `// Responses API` sub-group already
follows. Pure reorder, no other changes.
Reorders the `defaultConfig` keys in `packages/data-schemas/src/app/service.ts`
shortest-line to longest-line, with the explicit-value entries
(`mcpConfig`, `fileStrategies`, `cloudfront`) trailing the shorthand
ones. Pure reorder, no behavior change.
@upman

upman commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

This is great! Should we do this filtering for tool call outputs too?

Another deterministic check we can do is check for invisible or suspiscious unicode characters in the output of tool calls and LLMs and remove them. This is used to present phishing web addresses to users.

Non deterministic, more complex checks could be integrated through API calls - for example google's model armor or a self-hosted Prompt-Guard-86M model for example.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@danny-avila

Copy link
Copy Markdown
Owner

@upman I think google's model armor is a good potential solution we can implement. I recently updated LibreChat to utilize hooks that we can tap into similar to Claude for this

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila danny-avila merged commit 5867f1a into dev Jun 10, 2026
29 checks passed
@danny-avila danny-avila deleted the feat/pii-filter-middleware branch June 10, 2026 13:03
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request Jun 18, 2026
* 🛡️ feat: Reject chat messages matching configured credential patterns

Adds an opt-in `messagePiiFilter` middleware mounted on the agent
chat route ahead of `moderateText`. When the configured patterns
match the user's input the request is refused with 400, so the
credential never reaches OpenAI moderation, the model, or MongoDB.
Three starter patterns ship by default and operators can subset
them or add their own regex via `customPatterns` in librechat.yaml.

* 🧪 test: Memoize compiled patterns + add middleware spec

Memoize the compiled pattern array via a WeakMap keyed by the
messagePiiFilter config object so repeat requests against the same
config skip the per-request RegExp construction. Cache entries are
released automatically when the config object itself rotates.

Adds packages/api/src/middleware/messagePiiFilter.spec.ts covering
the default-starter rejections, the starterPatterns subset and
empty-array semantics, customPatterns matching layered on top of and
in place of the starters, the no-config and empty-text pass-through
paths, and a memoization regression check.

* 🛡️ fix: Skip invalid customPattern regexes instead of crashing the request

Admin DB overrides for `messagePiiFilter.customPatterns` reach
`req.config` via `mergeConfigOverrides`, which deep-merges raw
override values without re-running `configSchema`. A typo'd regex
like `(` would slip past the YAML-load validation and throw inside
`new RegExp(...)` during `compile()`, returning 500 for every chat
request until the operator rolled the override back.

Wrapped the per-pattern compile in a try/catch that logs the
invalid pattern id + reason and skips it, so other valid patterns
(starters and other custom entries) keep filtering. Added a
regression test alongside the existing spec.

* 🛡️ feat: Extend PII filter to OpenAI-compatible and Responses agent APIs

The chat-route middleware operates on `req.body.text`, but the remote
agent API endpoints (`/api/agents/v1/chat/completions`,
`/api/agents/v1/responses`) accept the same prompt content as a
`messages` array or an `input` field. A caller using their API key
could send a credential-shaped value through either route and bypass
the configured PII filter even though they share the same agent and
model backbone the middleware is meant to guard.

Factored out `findPiiMatchInMessages`, a tolerant walker that handles
both `content: string` and `content: ContentPart[]` user-message
shapes against the same compiled, cached pattern list. Wired it into
the OpenAI-compat controller after agent lookup and into the
Responses controller right after `convertToInternalMessages`. Each
returns the endpoint's native 400 error shape
(`sendErrorResponse` / `sendResponsesErrorResponse`) with the
`message_pii_filter_block` code when a user message matches.

* 🩹 test: Add findPiiMatchInMessages to OpenAI + Responses controller mocks

The OpenAI-compat and Responses controller specs mock `@librechat/api`
with a hand-listed object. The new `findPiiMatchInMessages` export
wired into both controllers in 3ea35af was missing from those
mocks, so the production lookup returned undefined and the controllers
threw at request time under jest. Added the missing entries (default
mock: returns null so the handlers fall through to the existing happy
paths). All 278 agents-controller tests pass locally.

* 🧹 refactor: Namespace messagePiiFilter under messageFilter.pii + fix import order

Renames the yaml field `messagePiiFilter` to `messageFilter.pii`, the
module to `messageFilterPii`, the factory to `createMessageFilterPii`,
the type to `MessageFilterPiiConfig`, and the error code to
`message_filter_pii_block`. The wrapper `messageFilter` namespace
gives future safety filters (e.g. `messageFilter.toxicity`) a place
to plug in without restructuring the config later. The
`findPiiMatchInMessages` helper kept its name because it already
describes what it does at the value level.

Also fixes import order Danny flagged on the OpenAI-compatible and
Responses controllers: `findPiiMatchInMessages` was appended at the
bottom of two `require('@librechat/api')` destructures rather than
placed in the length-sorted slot the house style expects.

* 🧹 chore: Length-sort the general require destructure in responses.js

Reorders the general sub-group inside the `require('@librechat/api')`
destructure shortest to longest so the whole block conforms to the
length-sort rule the file's `// Responses API` sub-group already
follows. Pure reorder, no other changes.

* 🧹 chore: Length-sort the defaultConfig block in AppService

Reorders the `defaultConfig` keys in `packages/data-schemas/src/app/service.ts`
shortest-line to longest-line, with the explicit-value entries
(`mcpConfig`, `fileStrategies`, `cloudfront`) trailing the shorthand
ones. Pure reorder, no behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants