HackBlue

Tools · LLM application security · 06 / 23

Log a suspected injection without the prompt

When a turn looks like an injection, store the channel, the source, a hash, and a short redacted excerpt. Leave the full prompt out if it may hold secrets.

12 min read · practice only with authorization

The prompt is often the sensitive part

A suspect turn is worth recording. The people who review it need to know which assistant, which channel, and whether a tool was blocked. They do not automatically need the whole prompt. The prompt holds the user’s question, the retrieved passage, and any tool result. Those spans can include customer names, mailbox contents, or a secret someone pasted into a wiki.

Impact of storing the raw prompt: the log becomes a second copy of that secret, often with a wider set of readers than the wiki had. Control: store enough to find the source and to show the decision, and redact or omit the rest.

A record that still explains the decision

Keep the time, the assistant name, the channel (user, retrieval, or tool), and a source id that points back to the document or the tool call. Hash the suspect span so two reviewers can tell whether they are looking at the same text without passing the text around. Store the length. If a tool was requested, store the tool name and the allow-or-block decision.

Cap any excerpt at 120 characters, and only after a redactor has run. If that pass replaces a secret-shaped token, drop the excerpt and keep the hash. A clean excerpt with no replacement can stay. If you would still not paste the remainder into a ticket, skip it.

Snippet
function event({ assistant, channel, source, span, tool, decision }) {
  const excerpt = redact(span).slice(0, 120)
  const safe = excerpt.includes('[redacted]') ? '' : excerpt
  return {
    assistant, channel, source,
    span_sha256: sha256(span),
    span_chars: span.length,
    excerpt: safe,
    tool: tool || null,
    decision,
  }
}

Redact before the row is written

Run the redactor in the application, before the logging client. Do not send the raw span to a log vendor and hope a downstream rule catches it. A simple pass replaces long unbroken tokens. Add patterns your organization already uses for secret scanning, and test them with fake values in a unit test, not with a live credential.

The hash is of the original span, computed locally, so you can match a later copy of the same document. Matching does not require the log to hold the span.

Worked case: a password pasted into a wiki page

A facilities page includes a database password a contractor pasted under a heading. Harbor Desk retrieves the page, and the model starts to propose opening a connection with that value. The operator flags the turn. The tool open_connection is not on the allowlist, so the call is blocked before any connection is attempted.

The log row names the assistant, the channel “retrieval,” the wiki page id, the tool name, and the decision “blocked.” The excerpt is empty because the redactor found a secret-shaped token, and the row stores the hash and the character count instead. The password is not in the log. The follow-up is outside the model: remove the password from the wiki, rotate it, and confirm the page no longer matches the hash.

Snippet
{
  "assistant": "harbor-desk",
  "channel": "retrieval",
  "source": "wiki:page-1842",
  "span_sha256": "<hash computed locally>",
  "span_chars": 640,
  "excerpt": "",
  "tool": "open_connection",
  "decision": "blocked"
}

Diagram

What the injection log keeps

The row should let a reviewer find the source and the decision.

  • Keep

    Time, assistant, channel, source id, tool name, and allow-or-block.

  • Hash

    A local hash of the span, plus its length, so the text can stay out of the row.

  • Excerpt

    At most a short redacted slice, and nothing if a secret pattern matched.

  • Leave out

    The full prompt, retrieved customer text, and any live credential.

Check yourself

1. Why avoid storing the full prompt in the injection log?

2. What do you keep so two reviewers can tell it is the same span?

3. When should the excerpt be left empty?