Untrusted input
What the renderer checks in the Block Kit JSON it draws, and what your server still has to check in every interaction, text and lookup.
Two kinds of data cross the renderer: the Block Kit JSON it draws, which comes from whoever wrote the payload, and the interactions it reports, which come from the browser. Neither is something your app should trust as is. This page lists what the package checks for you and what stays your job.
What the renderer checks
- Markup: the renderer builds every block with React elements and never sets HTML from a payload, so text in a block can’t inject markup or script.
- Link URLs: links, link buttons and overflow options keep only
http,https,mailto,slackand relative URLs. Any other scheme is dropped without an error: the link keeps its text and loses itshref, and a button still sends its action but opens nothing. See URL safety for the full list. - Image URLs are left as they are, so an
image_urlmakes the viewer’s browser load whatever host the payload names. Render payloads from people you don’t trust only where that’s acceptable, or replace the image block with one that checks the host (see Custom blocks).
Fields your own custom blocks read are not checked by the renderer; check them in the component.
Interactions come from the browser
onAction, onPayload, onSubmit, onClose and onOptions receive payloads the provider builds
in the page. Anyone who can open the page can change them, or send your server the same request
without the page at all. On the server:
- Accept a request as Slack’s only when its signature checks out. Slack signs every request
with your app’s signing secret (Verifying requests from Slack);
Bolt does this for you. The
tokenfield in a payload is Slack’s deprecated verification token and proves nothing. A payload the interaction relay signed proves only that it passed through the relay, which signs whatever it’s sent. - Decide who is acting from your own session, not from the payload’s
user,teamoridentityfields. Then check that this person may do what the action asks: approving a deploy in the browser is not an authorization check. - Check each value against what you offered. A selected option should be one of the options you
sent (or your options load URL returned), a user or channel ID one the person may pick, a date in
the range you accept. Without an
onOptionshandler the preview lets someone type any value into anexternal_selectand press Enter, and a users, conversations or channels select accepts a typed ID; a forged request can carry anything. - Enforce limits again.
max_selected_items,min_length,max_length,min_valueandmax_valueshape what the controls let someone enter, and validation checks them before submitting, but a request built by hand skips both. Slack’s own limits are in Limits. - Treat
file_inputvalues as claims. The preview reports each file’s name, extension and size as the browser gives them and uploads nothing;filetypesonly filters the file picker. Check the real file’s type and size where you receive it. - Bound the work an
onOptionsrequest triggers. The renderer waits 250 ms after the last keystroke and drops answers to stale queries, but your options endpoint can be called directly: cap the length ofvalue, return at most 100 options (Slack’s limit), and rate limit it like any search endpoint.
Text you put into mrkdwn
Slack reads <…> in mrkdwn as a link, a mention or a special command such as <!channel>, and
the renderer does the same. When you build a message from text someone else typed, escape &, <
and > the way Slack asks (escaping text),
so their text can’t add a link or ping a channel:
const escapeMrkdwn = (text: string) =>
text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
const text = `*New comment:* ${escapeMrkdwn(comment)}`;
Bold, italics and the other formatting characters stay; escaping only stops the text from becoming
a link or a mention. To show it with no formatting at all, put it in a plain_text object instead.
Resolvers return your data to the page
Resolvers run in the browser, and whatever they return is in the page for anyone who can open it.
directory,useranduserProfile: return only the people and channels the viewer may see, and only the fields you mean to show. Names, avatars, presence, titles and time zones of everyone in a workspace are personal data.createResolverRelayalready limits its answers to an allowlist of fields, and itstokenkeeps strangers from querying it; a directory you build yourself needs the same care.slackFile: return a route on your own server that checks access, neverurl_privateor a URL with a token in it. Slack files lists what the route checks.imageSize: don’t have your server download a payload’simage_urlto measure it. A payload can name an internal address, and the request reveals your server’s IP to the host. Return a size you already know, such as one stored when the image was uploaded, orundefined.
Reporting a vulnerability
Report a problem in the package itself through SECURITY.md,
which also lists what counts.
Related
Connecting your app
Relay interactions to a real Slack app, and keep the signing secret on the server.
Payloads
The interaction payloads the provider builds, and their identity fields.
Validation
The client-side checks an input block runs before a submission.
URL safety
The URL schemes the renderer follows, and what it does with the rest.