This release includes 6 security fixes for security teams reviewing exposed deployments.
Topics
+14 more
Affected surfaces
ReleasePort's take
Moderate signalThe release fixes plaintext secret disclosure and redacts sensitive names in the /settings/api/bulk endpoint.
Why it matters: Fixes severityβ90 plaintext secret leakage and severityβ85 redaction of nonβclassic secrets (client_secret, bearer_token) in GET /settings/api/bulk responses; critical for SREs securing API endpoints.
Summary
AI summaryBroad release touches π Bug Fixes, Other Changes, π Other Changes, and π Security Updates.
Changes in this release
| Type | Severity | Summary | CVE |
|---|---|---|---|
| Security | Critical |
Fixes plaintext secret disclosure in GET /settings/api/bulk endpoint. Fixes plaintext secret disclosure in GET /settings/api/bulk endpoint. Source: llm_adapter@2026-07-15 Confidence: low |
β |
| Security | High |
Redacts nonβclassic secret names (client_secret, bearer_token, etc.) in bulk settings response. Redacts nonβclassic secret names (client_secret, bearer_token, etc.) in bulk settings response. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Security | High |
Escapes LIKE wildcards in userβsupplied strings to prevent wildcard injection. Escapes LIKE wildcards in userβsupplied strings to prevent wildcard injection. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Security | High |
Scrubs exception text in /api/v1/quick_summary and /api/v1/analyze_documents responses. Scrubs exception text in /api/v1/quick_summary and /api/v1/analyze_documents responses. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Security | High |
Routes all LLM, embedding provider, and search engine logs through diagnoseβgated secure logging wrapper. Routes all LLM, embedding provider, and search engine logs through diagnoseβgated secure logging wrapper. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Security | High |
Prevents plaintext secret disclosure in GET /settings/api/bulk by redacting nested API keys and escaping LIKE wildcards. Prevents plaintext secret disclosure in GET /settings/api/bulk by redacting nested API keys and escaping LIKE wildcards. Source: granite4.1:30b@2026-07-15-audit Confidence: low |
β |
| Feature | Medium |
Adds Sofya as an optional hosted webβsearch engine. Adds Sofya as an optional hosted webβsearch engine. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Bugfix | Medium |
Preserves spaces in SearXNG result titles and snippets. Preserves spaces in SearXNG result titles and snippets. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Bugfix | Medium |
Fixes citation labels rendering as empty `[]` for library/RAG sources. Fixes citation labels rendering as empty `[]` for library/RAG sources. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Bugfix | Medium |
Corrects formatBytes to display sizes β₯β―1β―TB correctly. Corrects formatBytes to display sizes β₯β―1β―TB correctly. Source: llm_adapter@2026-07-15 Confidence: high |
β |
| Bugfix | Medium |
Rolls back partial session on postβauthentication failure to avoid silent login. Rolls back partial session on postβauthentication failure to avoid silent login. Source: llm_adapter@2026-07-15 Confidence: high |
β |
Full changelog
New search engine, settings-API secret leak fixes, and a batch of security hardening
Security fixes
Several defense-in-depth gaps were closed this release β all scoped to a user's own data, but worth upgrading for:
GET /settings/api/bulkcould return your configured API keys in plaintext: namespace requests (e.g.keys[]=llm) skipped redaction of nested*.api_keyvalues, andkeys[]=%could dump the whole settings table via an unescapedLIKE(#5028). Non-classic secret names likeclient_secret,bearer_token, andapi_secretare now also redacted by the bulk endpoint (#5038).- User-supplied strings reaching SQL
LIKEqueries (library search/domain filters, news subscription filters, and more) now escape%and_, so searches for values like100%ora_bmatch literally instead of acting as wildcards (#3094). - Exception text returned by
/api/v1/quick_summaryand/api/v1/analyze_documentsis now scrubbed at the HTTP boundary, so error payloads can no longer echo credentials or stack-trace fragments to API clients (#5032). - All LLM provider, embedding provider, and search engine modules now log through the diagnose-gated secure-logging wrapper β production logs no longer receive full tracebacks (which can echo API keys) from provider/engine failures unless diagnose mode is explicitly enabled (#4976).
- Registration/login no longer leave you silently half-logged-in when post-authentication setup fails partway; the partial session is rolled back so the reported failure matches reality (#5006).
Bug fixes
- SearXNG titles no longer lose their spaces β multi-word result titles were rendering as
WordOneWordTwoWordThreein the## Sourcesblock of saved reports; snippets had the same issue. This also improves relevance filtering and candidate extraction on SearXNG-sourced collections, which previously saw single-token titles (#5022). - Citation labels for library/RAG sources no longer render as empty
[]links β relative URLs like/library/document/<uuid>now fall back to a slugified document title so the citation carries the document name (#4880). Web citations are unchanged. - The
langgraph-agentstrategy now showsπ Reading the page: "<url>"when the agent fetches a URL, instead of the misleadingπ Searching Fetch Content(#5011). It also caps subtopic research at the documented limit of 5 and, when the model requests more, names the dropped subtopics in logs, the UI, and the tool reply so uninvestigated topics don't get cited (#5013). - The Research Logs panel now keeps old warnings and errors when its 500-entry cap is hit, dropping routine info entries first (#4900), and library sizes of 1 TB or more no longer display as
"1 undefined"(#4982). - The news subscription-history endpoint no longer errors for subscriptions with research runs (#3094).
Under the hood, importing the citation formatter and url_classifier no longer drags in the full fetch stack (requests, playwright, bs4, β¦) thanks to lazy ContentFetcher resolution (#5023, #5041), and the release pipeline now pins PyPI builds and git tags to the exact gated release commit so published wheels can never drift ahead of the tag (#5026).
π Security
- User-supplied strings that reach SQL
LIKEqueries β library search and domain filters, domain-classifier sampling, and news subscription filters β now escape%and_wildcards. Previously a value like100%ora_bwas interpreted as a wildcard and matched unrelated rows; searches and filters now match literally, closing a wildcard-injection/enumeration vector within a user's own database. (#3094) - All LLM provider, embedding provider, and web search engine modules now log through the diagnose-gated
security.secure_loggingwrapper, so exception tracebacks in these paths only appear in logs when diagnose mode is explicitly enabled (LDR_APP_DEBUG+LDR_LOGURU_DIAGNOSE) β production logs no longer receive full tracebacks that could echo API keys or request internals from provider/engine failures. Thecheck-sensitive-loggingpre-commit hook now enforces this by construction: rawloguruimports and every known bypass route (logger.opt(),@logger.catch, private logger handles,traceback/sys.exc_info()interpolation, dynamic loguru/traceback imports, logger rebinding, andgetattrdodges) are rejected in these directories,bind()/patch()chain messages get the same exception-scrubbing checks as direct logger calls, and exception detail reached through attribute or subscript access (e.args,e.response.text,e.strerror) is flagged likestr(e)itself. (#4976) - Fixed a plaintext-secret disclosure in
GET /settings/api/bulk: a namespace request (e.g.keys[]=llm) returned nested*.api_keyvalues unredacted because the redaction check looked only at the outer requested key, andkeys[]=%could dump the whole settings table via an unescapedLIKE. Redaction now recurses into setting subtrees and the key lookup escapes LIKE wildcards. (#5028) GET /settings/api/bulknow redacts password-typed settings whose leaf name is a non-classic secret token (e.g.client_secret,secret_key,bearer_token,api_secret,app_secret). The bulk endpoint redacts by key name only, so these were previously shipped in the clear while the singular GET masked them. (#5038)- Exception-derived strings flowing through the
/api/v1/quick_summaryand/api/v1/analyze_documentsJSON responses are now scrubbed at the HTTP boundary (and at the strategy layer inSourceBasedSearchStrategy) viasanitize_error_for_client, closing a CWE-209 information-exposure vector flagged by CodeQL (#8019): a caught exception interpolated into a strategy's error fields could otherwise reach the API client β assummary(the keyquick_summary()/analyze_documents()actually return),formatted_findings, or per-findingcontentβ with credentials or stack-trace text intact. Credentials are scrubbed and length is capped; the"Error: "prefix is preserved so error payloads stay recognizable to clients, and the strategy-layer scrub independently covers the web-UI SSE path (research_service.pyβErrorReportGenerator), which consumes strategy output directly./api/v1/generate_reportgets the same boundary scrub purely as a precaution β its current payload (content/metadata) carries no error fields.
β¨ New Features
- Sofya search engine β Added Sofya as an optional hosted web-search engine. Sofya's
/v1/searchreturns ranked results with SERP snippets and extracted page content (markdown) in a single call, fitting LDR's two-phase retrieval model without a separate fetch round trip. Supportsbasic/snippetsdepth,general/newstopics, recency filtering, and domain include/exclude. Configure your API key under Settings β Search β Sofya (search.engine.web.sofya.api_key) or theLDR_SEARCH_ENGINE_WEB_SOFYA_API_KEYenvironment variable; GitHub sign-up grants 1000 free credits/month.
π Bug Fixes
- Fixed the news subscription-history endpoint (
/news/api/subscriptions/<id>/history) raising an internal error for any subscription that had research runs:get_subscription_historycalled.isoformat()on thecreated_at/completed_atvalues, which are stored as isoformat strings, so it now returns them directly. (#3094) - Fix collapsed-whitespace bug in SearXNG-sourced titles. The HTML parser was calling
BeautifulSoup.Tag.get_text(strip=True)to read result titles, which strips leading/trailing whitespace and collapses every internal whitespace run to nothing β so multi-word titles like "Word One Word Two Word Three" rendered as "WordOneWordTwoWordThree" in the## Sourcesblock. Swapped toget_text(" ", strip=True)(the documented BeautifulSoup idiom for "strip edges, replace internal runs with a single space") so word boundaries survive. The same fix was applied to result snippets. Title preservation flows through to the LLM relevance filter andBaseCandidateExplorer/ProgressiveExplorercandidate-phrase extraction, both of which previously saw single-token titles from SearXNG-sourced collections. (#4970) - Registration/login no longer leave a user silently logged in when post-authentication setup fails partway: the partial session is now rolled back, so a reported failure matches reality (you are not logged in). (#5006)
- Fix citation labels rendering as empty
[]for library/RAG sources. TheDOMAIN_HYPERLINKS,DOMAIN_ID_HYPERLINKS, andDOMAIN_ID_ALWAYS_HYPERLINKSmodes previously fedurlparse(url).netlocstraight into the citation label, so any relative URL (most notably RAG / library hits that look like/library/document/<uuid>) produced an empty string. With a single relative-URL citation the label collapsed to[[]](url)and rendered as a[]link with no anchor text; with multiple relative-URL citations the suffix leaked out as[[-1]](url),[[-2]](url). The formatter now falls back to a slugified (lowercased, alphanumeric-only) version of the document title, and as a final fallback to the citation number itself, so the label is always non-empty and the link carries the document name. Real web URLs (arxiv.org, github.com, etc.) are unchanged β the domain still wins whennetlocis non-empty. - Fixed
formatBytesrendering sizes of 1 TB or larger as"N undefined"(e.g. a library blob total of 1 TB showed as1 undefined). The shared byte formatter only defined units up to GB; it now covers TB through EB and clamps the unit index at both ends, so very large values no longer index past the end of the unit list and sub-1-byte values no longer produce a negative index. - When the Research Logs panel hits its 500-entry DOM cap, the oldest entries are flushed first β which previously meant old warnings and errors got dropped even when the cap was blown by a flood of routine info entries. The prune now walks the cap-excess slots in priority order (info first, then milestones, then warnings, then errors), so the panel keeps its most diagnostic entries even on long research runs.
π Other Changes
- Removed the
importlib.utildisk-load workaround forurl_classifierincitation_formatter.py(introduced in #4880). Now thatcontent_fetcherno longer eagerly imports its heavy dependency tree (#5023), the formatter uses a normalfrom ..content_fetcher.url_classifier import URLClassifier, URLType. Also fixed two nits in the #5023 test file: an off-by-one project-root path in the subprocess tests andcheck=Truecalls that hid the subprocess stderr from failure reports. (#4992) - Fixed an order-dependent test flake: the security import-fallback tests replaced
local_deep_research.security*modules with fresh copies, recreating enums likeSensitivityand breaking identity checks in tests that ran later on the same worker. - Source-tagged citation formatting no longer re-runs the URL-classifier label lookup for every citation occurrence β the pre-computed label cache is consulted lazily, so each source's label resolves once. (The disk-loading import this originally worked around was removed entirely in #4992.)
- The Sofya egress-labels test now compares enum members by
.valueinstead of by identity. Under the Docker test setup the package is simultaneously available from/app/src(PYTHONPATH) and the installed wheel, soSofyaSearchEngine's class attribute and the test'sSensitivitycan resolve to distinct class objects with identical members. Python'sEnum.__eq__returnsNotImplementedacross distinct classes (so==fails just likeis); only.value(or.name) survives the dual-class scenario. - The
check-pathlib-usagepre-commit hook now catchesimport os as <alias>followed by<alias>.path.*. Previously the AST matcher only flagged the literal nameos, so an aliased import silently bypassed the check β which is how a_os.path.join(...)slipped into #4880 and had to be caught by human review instead. content_fetcher.ContentFetcheris now resolved lazily on first attribute access (PEP 562 module__getattr__) instead of being eagerly imported in the package__init__.url_classifier(and any other import that goes through thecontent_fetcherpackage) no longer drags in the fetcher's transitive deps (requests,playwright,bs4,lxml, etc.), so minimal test setups and the citation formatter can stop using the disk-load workaround introduced in #4880. Public API is unchanged.
What's Changed
π Security Updates
- feat(security): enforce diagnose-gated logging wrapper in provider/engine paths (#4183) by @Donny-Guo in https://github.com/LearningCircuit/local-deep-research/pull/4976
- security: escape LIKE wildcards across library, domain classifier, news, and journal_quality by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/3094
- fix(auth): roll back partial session so a post-creation failure isn't a silent login by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5006
- security: recognize client_secret/bearer_token/api_secret/β¦ across settings + error-log redaction by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5038
- fix(security): scrub exception data at API boundary + close pending CVEs by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5032
- chore(dev): consolidate server scripts into restart_server.sh flags by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4953
β¨ New Features
- feat(search): add Sofya search engine by @yusufgurdogan in https://github.com/LearningCircuit/local-deep-research/pull/4434
- feat(release): complete signal-ordered diff input + hallucination guard for AI release notes by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5027
π Bug Fixes
- fix(hooks): catch aliased os imports in check-pathlib-usage by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/4994
- fix: formatBytes shows "undefined" for sizes >= 1 TB by @Osamaali313 in https://github.com/LearningCircuit/local-deep-research/pull/4982
- fix(logpanel): preserve warnings/errors when cap prunes the log by @skbs-eng in https://github.com/LearningCircuit/local-deep-research/pull/4900
- fix(citations): fall back to title slug when citation URL has no domain by @skbs-eng in https://github.com/LearningCircuit/local-deep-research/pull/4880
- fix(langgraph-agent): route fetch_content calls through the "π Reading" branch by @skbs-eng in https://github.com/LearningCircuit/local-deep-research/pull/5011
- fix(release): keep release-artifacts 7 days so approval delay cannot expire them by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5016
- perf(citations): resolve source-tag labels from cache lazily by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5007
- fix(langgraph-agent): align MAX_SUBTOPICS with 'pass 2-5' contract and make truncation observable (#5012) by @skbs-eng in https://github.com/LearningCircuit/local-deep-research/pull/5013
- fix(test): restore original security modules after import-fallback tests by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5029
- fix(search-engine-searxng): preserve word boundaries in parsed titles (#4970) by @skbs-eng in https://github.com/LearningCircuit/local-deep-research/pull/5022
- test(sofya): compare egress labels by value, not identity by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5031
β‘ Performance Improvements
- refactor(content_fetcher): defer ContentFetcher import via PEP 562 getattr (#4992) by @skbs-eng in https://github.com/LearningCircuit/local-deep-research/pull/5023
- test(citations): fresh-interpreter guard that citation_formatter import stays light by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5036
π Documentation
- docs(release): correct retention comment β effective approval window is 5 days, bounded by SLSA provenance by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5035
π§ CI/CD & Maintenance
- chore: bump patch version to 1.9.1 by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/4993
- chore: clear changelog fragments for 1.9.0 by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5017
- π€ Update dependencies by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/5009
- π€ Update dependencies by @github-actions[bot] in https://github.com/LearningCircuit/local-deep-research/pull/5020
- ci(changelog): fail loud on undeclared towncrier fragment categories by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5045
Other Changes
- security: stop /settings/api/bulk leaking nested plaintext secrets by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5028
- refactor(security): add shared scrub_error() dual-scrub helper (#4183 follow-up) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5019
- refactor(citations): replace url_classifier disk-loader with a normal import (#4992) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5041
- refactor(strategies): remove dead error-handler helpers with raw exception interpolation by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5042
- fix(security): explicit grype ignore for CVE-2026-4360 (NVD-CPE match has fix-state unknown) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5044
- fix(test): restore parent-package security attribute when snapshot is empty by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5037
- fix(security): grype ignore for CVE-2026-15308 (CPython html.parser DoS) by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5046
- fix(release): pin PyPI build to the release commit, not dispatch-time HEAD by @LearningCircuit in https://github.com/LearningCircuit/local-deep-research/pull/5026
New Contributors
- @Osamaali313 made their first contribution in https://github.com/LearningCircuit/local-deep-research/pull/4982
- @yusufgurdogan made their first contribution in https://github.com/LearningCircuit/local-deep-research/pull/4434
Full Changelog: https://github.com/LearningCircuit/local-deep-research/compare/v1.9.0...v1.9.1
Security Fixes
- CVEβ2026βXXXXX β `GET /settings/api/bulk` redacts nested API keys and nonβclassic secrets such as client_secret, bearer_token, api_secret.
- SQL LIKE wildcard injection fixed: userβsupplied strings now escape `%` and `_` in library search, domain filters, news subscription filters (closes enumeration vector).
- Exception text from `/api/v1/quick_summary` and `/api/v1/analyze_documents` is scrubbed at the HTTP boundary to prevent credential leakage.
- LLM provider, embedding provider, and search engine modules now log through diagnoseβgated `security.secure_logging` wrapper; production logs no longer emit full tracebacks that could expose API keys.
- CVE-2026-4360
- CVE-2026-15308
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Local Deep Research
AI-powered deep research tool with multi-source search (arXiv, PubMed, web)
Related context
Related tools
Earlier breaking changes
- v1.6.11 JavaScript rendering disabled by default in production Docker image; new web.enable_javascript_rendering setting (default false).
Beta — feedback welcome: [email protected]