microsoft / microsoft/simplechat

Root DEBUG logging writes workspace document content to stdout

Open
#1,339 1 comment 0 reactions 1 assignee View on GitHub

@Bionic711 is already working on this.

Since Aug 22, 2026.

bug
Dominant language
Python
Stars
152
Forks
116
Avg merge
7h 7m
Merged PRs (30d)
122

Description

**Severity: high. Active data exposure in every deployment by default. Fix available.**

`application/single_app/app.py:310` calls:

```python
logging.basicConfig(level=logging.DEBUG)
```

That sets DEBUG on the **root** logger, so every third-party library inherits it. Three
separate libraries then write retrieved workspace document text into the process log:

- `openai._base_client` logs `"Request options: %s"` with a dump of the request body,
which contains the assembled `messages` array and therefore every retrieved document
excerpt in the prompt.
- `semantic_kernel.prompt_template.kernel_prompt_template` logs `"Rendered prompt: ..."`,
the fully rendered prompt text.
- `semantic_kernel.functions.kernel_function_log_messages` logs `"Function arguments: %s"`
and `"Function result: %s"`, which for a retrieval plugin is document text.

A fourth, `azure.core.pipeline.policies.NetworkTraceLoggingPolicy`, appends
`"Request body:"` plus the body. It is double gated behind a `logging_enable` flag so it
does not fire by default, but Cosmos DB and Azure AI Search bodies are document chunk
text, so it belongs in the same category.

A second instance of the same line exists at
`application/single_app/simplechat_scheduler.py:28`, so the scheduler process leaks the
same way.

### Why this matters more than log noise

SimpleChat's tenant boundary is per-workspace document isolation, enforced at query time
by the OData filters in `functions_search.py`. The log bypasses that boundary entirely.

On a deployed host, stdout is the platform log sink: App Service Log Stream, or container
output collected into Log Analytics. Those sinks carry their own access control and their
own retention. So a person with Reader on the resource, or query rights on the log
workspace, and no membership in a workgroup, can read that workgroup's documents out of
the logs. Retention means they can still read them after the document has been deleted
from the application.

Nothing has to be misconfigured for this to happen. It is the default behavior.

### Measured on a running instance

One grounded chat request produced 587,374 bytes of log, containing one full
`Retrieved Excerpts:` block, one `Rendered prompt:` record, two
`openai._base_client` request-body dumps, and verbatim sentences from the source
document. After the fix the same request produced 132,257 bytes with zero of each, a 77
percent volume reduction alongside the containment.

Credentials are not affected. The openai client redacts its own auth header, logging
`'api-key': ''`.

### Why the fix is safe, which is the non-obvious part

Removing the global DEBUG does not silence SimpleChat's own debug output, because that
output never depended on the root logger level:

- `functions_appinsights.py` `_emit_debug_message` writes with plain
`print(debug_msg, flush=flush)`, gated on the `enable_debug_logging` setting. It never
touches the `logging` module.
- `_get_appinsights_debug_logger` builds a child logger and calls
`setLevel(logging.DEBUG)` on it directly, so App Insights debug traces are unaffected.
- A comment in `_emit_appinsights_debug_trace` already states the intent: "Use a child
logger so DEBUG traces can flow to App Insights even when the parent logger stays at
INFO to avoid broad third-party debug noise."
- The setting `enable_debug_logging`, default `False`, already exists in
`functions_settings.py`.

So `basicConfig(level=logging.DEBUG)` contributes nothing to the application's own debug
output and directly contradicts the design intent of the module beside it. It is the
defect, not a feature that needs replacing.

**Suggested fix.** Hold the root logger at INFO and pin the content-emitting namespaces
(`openai`, `semantic_kernel`, `azure.core.pipeline.policies`) to an INFO floor so raising
the root level later cannot reopen the leak. Keep the `basicConfig` call rather than
deleting it, because with no root handler installed records fall through to `lastResort`,
which discards anything below WARNING and would lose ordinary INFO output in local
development. Pin package roots rather than private module paths, since names like
`_base_client` are internal and can be renamed in a patch release. Provide an explicit
environment-variable opt-in for wire-level troubleshooting, and log a warning when it is
active so an operator who leaves it on has a record. Put the policy in one shared helper
called from both entrypoints rather than duplicating it.

One deliberate consequence worth noting: four `logger.debug` calls in `utils_cache.py`
stop printing by default, because they were only ever visible as a side effect of the
global DEBUG. One of them embedded a 50 character preview of the user's search text, so
containing it is a small additional benefit. The opt-in restores all four.

---

Found while enabling features on a fork of `v0.250.001`, verified against commit `ff8059163e09ede433003b1ed2822061c41239fe`. Line numbers are against that baseline.

We have a working fix and a functional test for this on our fork. Happy to open a PR if that is useful, or to share the patch and let you take a different approach. No expectation either way.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.