ICP-by-platform · Slack
Prompt-injection scanner for Slack bots
Slack is the default communication layer in thousands of AI-augmented engineering and support teams. Bots built with the Bolt SDK for JavaScript or Python listen for file_shared, message.files, and app_mention events. When a user uploads an image to a channel or DMs the bot with an attached screenshot, the bot fetches the image from Slack's API and — in the common pattern — forwards it directly to GPT-4o, Claude 3 Sonnet, or Gemini 1.5 Flash for captioning, classification, or agentic decision-making. Any workspace member — an employee, a guest, or an external contractor — can craft an image containing pixel-level adversarial instructions that redirect the bot's LLM output. A Glyphward scan inserted between the Slack file fetch and the LLM call eliminates this attack surface with a single await.
TL;DR
After fetching the image binary from Slack using the private download URL and your bot token, call POST https://glyphward.com/v1/scan with the base64-encoded image before passing it to the LLM. If the returned score ≥ 70, respond to the Slack event with a rejection message and do not call the LLM. Free tier — 10 scans/day, no card required.
Why Slack bots are a high-risk multimodal PI surface
Trust boundary mismatch. Slack workspaces routinely include external guests — agency partners, contractors, client representatives — who have permission to post files. An LLM-powered bot that trusts workspace membership as a proxy for trustworthy content is applying the wrong security model. Workspace guest access is an explicit Slack feature, not an edge case.
AI-augmented Slack workflows are now mainstream. Common patterns that create the attack surface include: screenshot-analysis bots that accept a screenshot from any channel member and return a summary or bug classification; design-review bots that forward Figma exports or UI mockups to a vision model for feedback; diagram-explanation bots that accept whiteboard photos and return structured notes; and support triage bots that accept error screenshot uploads from customers via Slack Connect channels.
Slack Connect channels extend the attack surface externally. Slack Connect lets external organisations participate in your internal channels. A support bot exposed to a Slack Connect channel accepts image uploads from any member of any connected external workspace — potentially hundreds of untrusted parties.
Integration: Bolt for JavaScript
The pattern below handles a file_shared event, fetches the image from Slack, scans it, and only calls the LLM if the scan passes.
const { App } = require('@slack/bolt');
const fetch = require('node-fetch');
const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET });
app.event('file_shared', async ({ event, client, say }) => {
// Fetch file metadata to get the private download URL
const fileInfo = await client.files.info({ file: event.file_id });
const privateUrl = fileInfo.file.url_private_download;
// Download the image binary using the bot token
const imageResp = await fetch(privateUrl, {
headers: { Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}` },
});
if (!imageResp.ok) {
await say({ channel: event.channel_id, text: 'Could not retrieve file for analysis.' });
return;
}
const imageBuffer = await imageResp.buffer();
const imageBase64 = imageBuffer.toString('base64');
// Scan for multimodal prompt injection before the LLM call
const scanResp = await fetch('https://glyphward.com/v1/scan', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GLYPHWARD_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ image: imageBase64, source: 'slack_bot' }),
});
if (!scanResp.ok) {
// Fail-closed: scanner unreachable → do not process image
await say({ channel: event.channel_id, text: 'Image could not be verified. Please try again shortly.' });
return;
}
const scan = await scanResp.json();
if (scan.score >= 70) {
await say({
channel: event.channel_id,
text: `This image was flagged by our security scanner (risk score ${scan.score}/100) and cannot be processed. If you believe this is a false positive, contact your workspace admin. Reference: \`${scan.scan_id}\``,
});
return;
}
// Safe: proceed with LLM call
const llmResult = await callVisionLLM(imageBase64);
await say({ channel: event.channel_id, text: llmResult });
});
(async () => { await app.start(3000); })();
The same pattern applies to app_mention events when the message payload includes a files array. Check event.files for any attached images and scan each before passing them to the LLM.
Integration: Bolt for Python
import os, base64, requests
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=os.environ["SLACK_BOT_TOKEN"])
@app.event("file_shared")
def handle_file_shared(event, client, say):
file_info = client.files_info(file=event["file_id"])
private_url = file_info["file"]["url_private_download"]
img_resp = requests.get(
private_url,
headers={"Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}"},
)
if not img_resp.ok:
say(channel=event["channel_id"], text="Could not retrieve file for analysis.")
return
image_b64 = base64.b64encode(img_resp.content).decode()
scan = requests.post(
"https://glyphward.com/v1/scan",
headers={"Authorization": f"Bearer {os.environ['GLYPHWARD_API_KEY']}",
"Content-Type": "application/json"},
json={"image": image_b64, "source": "slack_bot_python"},
timeout=5,
)
if not scan.ok or scan.json().get("score", 100) >= 70:
scan_id = scan.json().get("scan_id", "unknown") if scan.ok else "scan-failed"
say(channel=event["channel_id"],
text=f"Image flagged or scanner unavailable. Ref: `{scan_id}`")
return
llm_result = call_vision_llm(image_b64)
say(channel=event["channel_id"], text=llm_result)
if __name__ == "__main__":
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
Coverage matrix
| Defence layer | Internal member upload | Slack Connect external | DM attachment | Slash command with file |
|---|---|---|---|---|
| Slack native content scanning | Malware/virus only | Malware/virus only | Malware/virus only | No |
| LLM system prompt instructions | Probabilistic (not reliable) | Probabilistic | Probabilistic | Probabilistic |
| Text-only scanner (LLM Guard, Lakera) | No — image bytes ignored | No | No | No |
| Glyphward pre-LLM scan | Yes — pixel-level | Yes | Yes | Yes |
Related questions
Does this add noticeable latency to bot responses?
Glyphward's scan endpoint returns in under 200 ms for images up to 4 MB. Slack's 3-second acknowledgement requirement for Events API payloads means you must acknowledge the event immediately and handle the scan + LLM call asynchronously. In practice: acknowledge the event immediately with ack(), then run the fetch → scan → LLM pipeline in the background. Post the result back to the channel when ready. Users of AI-powered Slack bots already expect a short wait for image analysis — the scan adds no perceptible delay.
Can I log flagged images for human review?
Yes. Store scan_id, the Slack file_id, the channel, and the submitting user's Slack UID in a persistent log (a database row, a private Slack channel message, or an audit webhook). Do not store the image binary itself unless required for review — reference the file_id and use Slack's files.info API to retrieve it during review. On Glyphward Team plan, flagged scans are available in the web dashboard for 30 days.
What about Slack's built-in AI features (Slack AI)?
Slack AI (the native summarisation and search product) is separate from bot integrations. Slack AI processes channel content server-side under Slack's own controls — it is not exposed to your bot's LLM integration. This scanning guide applies specifically to bots you build using the Bolt SDK that make their own LLM API calls.
Does this work with Slack's Workflow Builder automations?
Slack Workflow Builder can trigger actions when a file is shared, but it cannot make arbitrary external API calls without a third-party integration step. For Workflow Builder-based automations that route images to AI, use a Make or Zapier step (see Make integration guide and Zapier integration guide) to insert the Glyphward scan between the Slack trigger and the AI action.
Further reading
- FigStep detection — the typographic attack class most likely to appear in screenshot-analysis bots.
- Indirect prompt injection via image — PI payloads in images shared from external URLs via Slack messages.
- For customer service AI — integration guide for helpdesk and support AI that also receives image attachments.
- For Make automation — scan images in Make scenarios that receive Slack file events via webhook.
- Multimodal LLM security API — general overview of Glyphward's scanning capabilities.
- Why text-only scanners miss image prompt injection — architectural background.