Slack: Stop unauthenticated requests from fataling the webhook endpoints - #811
Closed
obenland wants to merge 3 commits into
Closed
Slack: Stop unauthenticated requests from fataling the webhook endpoints#811obenland wants to merge 3 commits into
obenland wants to merge 3 commits into
Conversation
…nts.
`announce.php` and `committers.php` pass `$_POST['token']` straight to
`hash_equals()`. Both are Slack outgoing-webhook endpoints, so they assume a
POST body carrying a token. A bare GET — which scanners send regularly — leaves
`$_POST` empty, and PHP 8 raises an uncaught `TypeError` instead of the PHP 7
non-string warning, filling the error log with fatals:
Uncaught TypeError: hash_equals(): Argument #2 ($user_string) must be of
type string, null given in .../dotorg/slack/announce.php:32
Guard both with an `isset()`/`is_string()`/non-empty check before comparing. An
array token (`token[]=x`) would otherwise trigger the same `TypeError`, and the
non-empty check avoids matching a misconfigured empty constant. The superglobal
stays inside `hash_equals()` so the comparison remains timing-safe.
Also `break` out of the token loop in `announce.php` after a match. `run()`
returns rather than exits, so the loop kept testing the remaining
`WEBHOOK_TOKEN_N` constants and would fire twice if two ever held the same
value.
Unauthenticated requests now get the same empty 200 that a wrong token already
produced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
obenland
force-pushed
the
slack/webhook-token-guard
branch
2 times, most recently
from
August 17, 2026 01:15
8e6a837 to
b2fbb94
Compare
…ndards.
Flatten `announce.php`'s braced `namespace {}` / `namespace Dotorg\Slack\Announce {}`
pair into a single unbraced declaration. The global block was never necessary: an
included file's namespace comes from its own declaration, not the include site, and
variable scope is namespace-independent, so `$wpdb` stays global either way. This
also clears the `ScopeIndent` errors that the whole file body carried, without
reindenting a single line of it.
Both handlers now document their actual authentication mechanism in a file-level
docblock and disable the two sniffs that cannot apply to them: these are standalone
endpoints that never load WordPress, so there is no `wp_unslash()` to call and no
nonce to verify. Slack authenticates with a shared token instead.
The rest are ordinary fixes: file and function docblocks, multi-line call formatting
in `get_avatar()`, single quotes on the SQL that needs no interpolation, brackets
around `++$i` in string concatenation, and `dirname( __DIR__, 2 )` in place of nested
`dirname()` calls, matching `props.php` and the Calendly webhook in this directory.
`committers.php` echoes `$_POST['user_name']` back in its JSON response, so strip
control characters from it and default it when absent.
Both files now report zero PHPCS errors and warnings against the full ruleset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
obenland
force-pushed
the
slack/webhook-token-guard
branch
2 times, most recently
from
August 17, 2026 01:29
ff0f8c4 to
89679a8
Compare
`get_avatar()` hashed whatever `slack_users` returned. When a Slack ID has no linked WordPress.org account both queries return null, so `trim( null )` raised a PHP 8.1+ deprecation into the same error log this branch is clearing out, and `hash( 'sha256', '' )` handed every unlinked user the identical `e3b0c442...` Gravatar. Bail as soon as the `slack_users` lookup comes up empty, so the unlinked case no longer passes null into the second `prepare()` for a `WHERE ID = 0` round-trip that can never match, and keep a second check for a linked account with no email on file. `run()` in lib.php only consults this hook when Slack has no profile image, and gates on the result, so an empty return simply leaves the icon unset. The query string was also missing a separator: `?s=96d=mm&r=G` parses as `s=96d=mm` with no `d` at all, so the size was garbage and the `mm` default-avatar fallback never reached Gravatar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
obenland
force-pushed
the
slack/webhook-token-guard
branch
from
August 17, 2026 01:35
89679a8 to
c148fe9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
announce.phpandcommitters.phppass$_POST['token']straight intohash_equals(). Both are Slack outgoing-webhook endpoints, so they assume a POST body carrying a token. A bareGET— which scanners send regularly — leaves$_POSTempty, and PHP 8 raises an uncaughtTypeErrorwhere PHP 7 only warned about the non-string argument:Nothing leaks — the token comparison can never succeed — but every probe of these URLs writes a fatal to the error log.
Changes
Guard both endpoints with an
isset()/is_string()/ non-empty check before the comparison:is_string()matters because an array token (token[]=x) would otherwise trigger the sameTypeErrorthat a?? ''fallback alone would not catch.hash_equals()rather than being copied to a local, so the comparison remains timing-safe and PHPCS continues to credithash_equals()as the sanitizer.Also
breakout of the token loop inannounce.phpafter a match.run()returns rather than exits, so the loop kept testing the remainingWEBHOOK_TOKEN_Nconstants; harmless while the tokens are distinct, but it would post the announcement twice if two were ever configured with the same value.Unauthenticated requests now get the same empty
200that a wrong token already produced.Notes
WordPress.Security.ValidatedSanitizedInput.InputNotValidatederror PHPCS already reported on both files — "possibly undefined superglobal array index", literally this bug — is gone, and no new sniffs are introduced. The remainingMissingUnslasherrors are pre-existing and not fixable here: these endpoints don't bootstrap WordPress, so there is nowp_unslash(). The rest ofdotorg/slack/carries them too.trac-bot.php,security-team.php, andcommunity-deputies-calendly-webhook.phpalready guard with?? ''/empty(), andsubgroup.phpguards its signature headers. These two files were the only remaining instances.api.wordpress.org/public_html/dotorg/— these files bootstrap outside WordPress and have no PHPUnit setup. Verification wasphp -l, a before/after PHPCS comparison, and an isolated check thatreturnat the top level of a bracednamespace {}block exits cleanly.🤖 Generated with Claude Code
Coding standards
The repository's branch linter checks changed lines only, and the fix above tripped it, so a second commit takes both files to zero PHPCS errors and warnings against the full ruleset — not just on the changed lines.
announce.phpnamespace flattened. The bracednamespace {}/namespace Dotorg\Slack\Announce {}pair left the entire file body at zero indentation inside a scope PHPCS expects indented, so every line in the file trippedGeneric.WhiteSpace.ScopeIndent. The global block was never necessary: an included file's namespace comes from its own declaration, not the include site, and variable scope is namespace-independent, so$wpdbstays global either way. Collapsing to one unbraced declaration clears that plusDisallowCurlyBraceSyntax,DisallowDeclarationWithoutName, andOneDeclarationPerFile— without reindenting a single body line.phpcs:disablefor the two sniffs that cannot apply: these endpoints never load WordPress, so there is nowp_unslash()to call and no nonce to verify.get_avatar(), multi-line call formatting on the two$wpdb->prepare()calls, single quotes on the SQL that needs no interpolation, brackets around++$iin string concatenation, anddirname( __DIR__, 2 )for the nesteddirname()calls — matchingprops.phpand the Calendly webhook in this directory.committers.phpechoes$_POST['user_name']back in its JSON response, so control characters are stripped and the value defaults when absent.Gravatar lookup
A third commit fixes two defects in
get_avatar(), both surfaced while reviewing the reformatting above.Unlinked accounts raised a deprecation and collided. When a Slack ID has no linked WordPress.org account, both queries return
null, sotrim( null )raised a PHP 8.1+ deprecation — into the same error log this branch is cleaning up — andhash( 'sha256', '' )returned the constante3b0c442…for every unlinked user, giving them all the same avatar. It now bails as soon as theslack_userslookup comes up empty, so that case no longer passesnullinto the secondprepare()for aWHERE ID = 0round-trip that can never match, with a second check for a linked account that has no email on file.run()only consults this hook when Slack has no profile image and gates on the result, so an empty return leaves the icon unset.The query string was missing a separator.
?s=96d=mm&r=Gparses as:So the size was a garbage value and the
mmdefault-avatar fallback never reached Gravatar. Corrected to?s=96&d=mm&r=G, which parses ass=96,d=mm,r=G.Verification
No test coverage exists under
api.wordpress.org/public_html/dotorg/— these files bootstrap outside WordPress and have no PHPUnit setup. Verification was:php -lon both files, and full-filephpcsreporting zero errors and zero warnings.slack-config.php, andlib.php, exercising the flattened namespace: no token, empty token, array token, valid token matching the first constant, valid token matching the second, and a wrong token. Confirmed the guard rejects every malformed case without a fatal; thatrun()fires on a match with theget_avatar()hook resolving in the right namespace andglobal $wpdbseeing the value set by the un-namespaced include; thatbreakstops the loop at the matching iteration; and that( ++$i )preserves the pre-increment.get_avatar()re-run againstnull,'', and a real address undererror_reporting( E_ALL ): no deprecation, empty return on the first two, and the corrected URL parsing back to the intended four parameters.Relationship to #802
#802 overlaps this PR on both files — it independently flattens the same namespace, converts the same
dirname()calls, and adds an equivalent token guard. Whichever lands first, the other needs a rebase. Unique here are thebreakafterrun( $_POST ), the rejection of an empty-string token, and theget_avatar()fixes above; #802 additionally hardens the five other handlers in this directory.