From 2a37433e3c2fbf0830352f3932c36b731400dffb Mon Sep 17 00:00:00 2001 From: Matthew Flaschen Date: Fri, 10 Jun 2016 16:51:42 -0400 Subject: [PATCH] SECURITY: Fix topic title visibility There was an edge case (deletion, then suppression of the same topic) where the topic title of a suppressed topic was not correctly restricted. This was because it wasn't going through the permission system, which I realized was a problem in theory, but didn't realize it was also a problem in practice. To ensure the topic titles of deleted topics is still visible (see T119234), I added view-topic-title. While I'm in here, allow seeing topic summary if topic is hidden. Hidden topics are fully displayed (including summary) when you access a topic directly. This inconsistency only affected direct view-topic-summary URLs. Bug: T137593 Bug: T119234 Change-Id: I15de3f50d95a6bedda8dfc984a643d0237405c89 --- FlowActions.php | 29 ++++++++++++++++++ i18n/en.json | 10 +++++++ i18n/qqq.json | 10 +++++++ includes/Formatter/RevisionFormatter.php | 7 +++-- includes/Log/ActionFormatter.php | 50 +++++++++++++++++++++++++++----- includes/Templating.php | 6 +++- 6 files changed, 100 insertions(+), 12 deletions(-) diff --git a/FlowActions.php b/FlowActions.php index 3524d6f..2e8ce37 100644 --- a/FlowActions.php +++ b/FlowActions.php @@ -21,6 +21,8 @@ use Flow\Data\Listener\RecentChangesListener; * no one can perform the action described by that key. * * root-permissions: similar to 'permissions', but applies to the last revision * of the root post (= the topic) for the revision the action is executed against. + * If root-permissions is omitted entirely, it doesn't affect what is allowed. + * However, if any keys are set, omitted keys are treated as prohibited. * * core-delete-permissions: array of rights, where any of those rights will * give you permission to do the action on a deleted board (isAllowedAny). * Empty string and omitted behave like 'permissions'. @@ -823,6 +825,7 @@ $wgFlowActions = array( ), 'root-permissions' => array( PostRevision::MODERATED_NONE => '', + PostRevision::MODERATED_HIDDEN => '', PostRevision::MODERATED_LOCKED => '', ), 'core-delete-permissions' => array( 'deletedtext' ), @@ -833,6 +836,32 @@ $wgFlowActions = array( 'modules' => array(), ), + // This is only used when we specifically want to see the topic title. If we're + // cascading from a post (to view a post we need to be able to view the topic), + // we'll use 'view' for both the post and topic root. Unprivileged users shouldn't + // be able to view a post in a deleted topic, but should be able to view the topic + // title. + 'view-topic-title' => array( + 'performs-writes' => false, + 'log_type' => false, // don't log views + 'rc_insert' => false, // won't even be called, actually; only for writes + 'permissions' => array( + // Everyone can see topic titles on existent boards, unless the + // version you're viewing is suppressed, or the most recent version + // is + PostRevision::MODERATED_NONE => '', + PostRevision::MODERATED_HIDDEN => '', + PostRevision::MODERATED_LOCKED => '', + PostRevision::MODERATED_DELETED => '', + PostRevision::MODERATED_SUPPRESSED => 'flow-suppress', + ), + 'core-delete-permissions' => array( 'deletedtext' ), + 'links' => array(), // @todo + 'actions' => array(), // view is not a recorded change type, no actions will be requested + 'history' => array(), // views don't generate history + 'modules' => array(), + ), + // Actions not tied to a particular revision change_type // or just move these to a different file // @todo: we should probably at least add 'permissions' in these below diff --git a/i18n/en.json b/i18n/en.json index 18ba35a..b1fd675 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -18,15 +18,25 @@ "flow-talk-taken-over-comment": "/* This page has been converted into a Flow discussion board */", "log-name-flow": "Flow activity log", "logentry-delete-flow-delete-post": "$1 {{GENDER:$2|deleted}} a [$4 post] on \"[[$3|$5]]\" on [[$6]]", + "logentry-delete-flow-delete-post-topic-title-not-visible": "$1 {{GENDER:$2|deleted}} a [$4 post] on [[$3|a topic]] on [[$5]]", "logentry-delete-flow-restore-post": "$1 {{GENDER:$2|restored}} a [$4 post] on \"[[$3|$5]]\" on [[$6]]", + "logentry-delete-flow-restore-post-topic-title-not-visible": "$1 {{GENDER:$2|restored}} a [$4 post] on [[$3|a topic]] on [[$5]]", "logentry-suppress-flow-suppress-post": "$1 {{GENDER:$2|suppressed}} a [$4 post] on \"[[$3|$5]]\" on [[$6]]", + "logentry-suppress-flow-suppress-post-topic-title-not-visible": "$1 {{GENDER:$2|suppressed}} a [$4 post] on [[$3|a topic]] on [[$5]]", "logentry-suppress-flow-restore-post": "$1 {{GENDER:$2|deleted}} a [$4 post] on \"[[$3|$5]]\" on [[$6]]", + "logentry-suppress-flow-restore-post-topic-title-not-visible": "$1 {{GENDER:$2|deleted}} a [$4 post] on \"[[$3|a topic]]\" on [[$5]]", "logentry-delete-flow-delete-topic": "$1 {{GENDER:$2|deleted}} topic \"[[$3|$5]]\" on [[$6]]", + "logentry-delete-flow-delete-topic-topic-title-not-visible": "$1 {{GENDER:$2|deleted}} [[$3|a topic]] on [[$5]]", "logentry-delete-flow-restore-topic": "$1 {{GENDER:$2|restored}} topic \"[[$3|$5]]\" on [[$6]]", + "logentry-delete-flow-restore-topic-topic-title-not-visible": "$1 {{GENDER:$2|restored}} [[$3|a topic]] on [[$5]]", "logentry-suppress-flow-suppress-topic": "$1 {{GENDER:$2|suppressed}} topic \"[[$3|$5]]\" on [[$6]]", + "logentry-suppress-flow-suppress-topic-topic-title-not-visible": "$1 {{GENDER:$2|suppressed}} [[$3|a topic]] on [[$5]]", "logentry-suppress-flow-restore-topic": "$1 {{GENDER:$2|deleted}} topic \"[[$3|$5]]\" on [[$6]]", + "logentry-suppress-flow-restore-topic-topic-title-not-visible": "$1 {{GENDER:$2|deleted}} [[$3|a topic]] on [[$5]]", "logentry-lock-flow-lock-topic": "$1 {{GENDER:$2|marked}} topic \"[[$3|$5]]\" as resolved on [[$6]]", + "logentry-lock-flow-lock-topic-topic-title-not-visible": "$1 {{GENDER:$2|marked}} [[$3|a topic]] as resolved on [[$5]]", "logentry-lock-flow-restore-topic": "$1 {{GENDER:$2|reopened}} topic \"[[$3|$5]]\" on [[$6]]", + "logentry-lock-flow-restore-topic-topic-title-not-visible": "$1 {{GENDER:$2|reopened}} [[$3|a topic]] on [[$5]]", "logentry-import-lqt-to-flow-topic": "[[$1|$2]] on [[$3]] was imported from LiquidThreads to Flow", "flow-user-moderated": "Moderated user", "flow-board-header-browse-topics-link": "Browse topics", diff --git a/i18n/qqq.json b/i18n/qqq.json index c24e5b3..c757d12 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -29,15 +29,25 @@ "flow-talk-taken-over-comment": "Comment of the new revision that is created when a page turns into a Flow discussion board.", "log-name-flow": "{{doc-logpage}}\nName of the Flow log filter on the [[Special:Log]] page.", "logentry-delete-flow-delete-post": "Text for a deletion log entry when a post was deleted. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-delete-flow-delete-post-topic-title-not-visible": "Text for a deletion log entry when a post was deleted. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-delete-flow-restore-post": "Text for a deletion log entry when a deleted post was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-delete-flow-restore-post-topic-title-not-visible": "Text for a deletion log entry when a deleted post was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-suppress-flow-suppress-post": "Text for a deletion log entry when a post was suppressed. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-suppress-flow-suppress-post-topic-title-not-visible": "Text for a deletion log entry when a post was suppressed. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-suppress-flow-restore-post": "Text for a deletion log entry when a suppressed post was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-suppress-flow-restore-post-topic-title-not-visible": "Text for a deletion log entry when a suppressed post was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the post was moderated\n* $4 - permalink URL to the moderated post\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-delete-flow-delete-topic": "Text for a deletion log entry when a topic was deleted. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-delete-flow-delete-topic-topic-title-not-visible": "Text for a deletion log entry when a topic was deleted. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-delete-flow-restore-topic": "Text for a deletion log entry when a deleted topic was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-delete-flow-restore-topic-topic-title-not-visible": "Text for a deletion log entry when a deleted topic was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-suppress-flow-suppress-topic": "Text for a deletion log entry when a topic was suppressed. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-suppress-flow-suppress-topic-topic-title-not-visible": "Text for a deletion log entry when a topic was suppressed. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-suppress-flow-restore-topic": "Text for a deletion log entry when a suppressed topic was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $4 - permalink URL to the moderated topic\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-suppress-flow-restore-topic-topic-title-not-visible": "Text for a deletion log entry when a suppressed topic was restored. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $4 - permalink URL to the moderated topic\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-lock-flow-lock-topic": "Text for a log entry when a topic was marked as resolved. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-lock-flow-lock-topic-topic-title-not-visible": "Text for a log entry when a topic was marked as resolved. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-lock-flow-restore-topic": "Text for a log entry when a resolved topic was reopened. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $4 - permalink URL to the moderated topic\n* $5 - The topic title text\n* $6 - The board page\n{{Related|Logentry-flow}}", + "logentry-lock-flow-restore-topic-topic-title-not-visible": "Text for a log entry when a resolved topic was reopened. Parameters:\n* $1 - the user: link to the user page\n* $2 - the username. Can be used for GENDER.\n* $3 - the page where the topic was moderated\n* $4 - permalink URL to the moderated topic\n* $5 - The board page\n{{Related|Logentry-flow}}", "logentry-import-lqt-to-flow-topic": "Text for an import log entry when a topic has been imported from LiquidThreads to Flow. Parameters:\n* $1 - The page within the topic namespace to which the topic was imported.\n* $2 - The title of the LiquidThreads thread being imported.\n* $3 - The board that was converted from LiquidThreads to Flow.", "flow-user-moderated": "Name to display when the current user is not allowed to see the users name due to moderation", "flow-board-header-browse-topics-link": "Text to show in the board description which links to the topics list.", diff --git a/includes/Formatter/RevisionFormatter.php b/includes/Formatter/RevisionFormatter.php index 4a68ef5..06fcb8d 100644 --- a/includes/Formatter/RevisionFormatter.php +++ b/includes/Formatter/RevisionFormatter.php @@ -1005,7 +1005,7 @@ class RevisionFormatter { } $root = $revision->getRootPost(); - if ( !$this->permissions->isAllowed( $root, 'view' ) ) { + if ( !$this->permissions->isAllowed( $root, 'view-topic-title' ) ) { return ''; } @@ -1026,7 +1026,7 @@ class RevisionFormatter { } $root = $revision->getRootPost(); - if ( !$this->permissions->isAllowed( $root, 'view' ) ) { + if ( !$this->permissions->isAllowed( $root, 'view-topic-title' ) ) { return ''; } @@ -1041,7 +1041,8 @@ class RevisionFormatter { /** @var PostRevision $post */ $post = $revision->getCollection()->getPost()->getLastRevision(); - if ( !$this->permissions->isAllowed( $post, 'view' ) ) { + $permissionAction = $post->isTopicTitle() ? 'view-topic-title' : 'view'; + if ( !$this->permissions->isAllowed( $post, $permissionAction ) ) { return ''; } diff --git a/includes/Log/ActionFormatter.php b/includes/Log/ActionFormatter.php index c39485b..5a12389 100644 --- a/includes/Log/ActionFormatter.php +++ b/includes/Log/ActionFormatter.php @@ -8,6 +8,7 @@ use Flow\Data\ManagerGroup; use Flow\Model\UUID; use Flow\Conversion\Utils; use Flow\Repository\TreeRepository; +use Flow\Templating; use Flow\UrlGenerator; use Message; @@ -18,11 +19,24 @@ class ActionFormatter extends \LogFormatter { static $uuids = array(); /** + * @var RevisionActionPermissions + */ + protected $permissions; + + /** + * @var Templating + */ + protected $templating; + + /** * @param \LogEntry $entry */ public function __construct( \LogEntry $entry ) { parent::__construct( $entry ); + $this->permissions = Container::get( 'permissions' ); + $this->templating = Container::get( 'templating' ); + $params = $this->entry->getParameters(); // serialized topicId or postId can be stored foreach ( $params as $key => $value ) { @@ -86,12 +100,32 @@ class ActionFormatter extends \LogFormatter { $title = $anchor->resolveTitle(); } + $rootLastRevision = $root->getLastRevision(); + // Give grep a chance to find the usages: - // logentry-delete-flow-delete-post, logentry-delete-flow-restore-post, - // logentry-suppress-flow-restore-post, logentry-suppress-flow-suppress-post, - // logentry-delete-flow-delete-topic, logentry-delete-flow-restore-topic, - // logentry-suppress-flow-restore-topic, logentry-suppress-flow-suppress-topic, - $message = $this->msg( "logentry-$type-$action" ) + // + // A few of the -topic-title-not-visible are not reachable with the current + // config (since people looking at the suppression log can see suppressed + // content), but are included to make it less brittle. + // + // logentry-delete-flow-delete-post, logentry-delete-flow-delete-post-topic-title-not-visible, + // logentry-delete-flow-restore-post, logentry-delete-flow-restore-post-topic-title-not-visible, + // logentry-suppress-flow-restore-post, logentry-suppress-flow-restore-post-topic-title-not-visible, + // logentry-suppress-flow-suppress-post, logentry-suppress-flow-suppress-post-topic-title-not-visible, + // logentry-delete-flow-delete-topic, logentry-delete-flow-delete-topic-topic-title-not-visible, + // logentry-delete-flow-restore-topic, logentry-delete-flow-restore-topic-topic-title-not-visible, + // logentry-suppress-flow-restore-topic, logentry-suppress-flow-restore-topic-topic-title-not-visible, + // logentry-suppress-flow-suppress-topic, logentry-suppress-flow-suppress-topic-topic-title-not-visible, + // logentry-lock-flow-lock-topic, logentry-lock-flow-lock-topic-topic-title-not-visible + // logentry-lock-flow-restore-topic, logentry-lock-flow-restore-topic-topic-title-not-visible, + $messageKey = "logentry-$type-$action"; + $isTopicTitleVisible = $this->permissions->isAllowed( $rootLastRevision, 'view-topic-title' ); + + if ( !$isTopicTitleVisible ) { + $messageKey .= '-topic-title-not-visible'; + } + + $message = $this->msg( $messageKey ) ->params( array( Message::rawParam( $this->getPerformerElement() ), $this->entry->getPerformer()->getName(), @@ -99,9 +133,9 @@ class ActionFormatter extends \LogFormatter { $title->getFullUrl(), // link to topic, higlighted post ) ); - // I don't think this is an actual security issue, but this should use Templating->getContent: T119234 - // topic title - $message->plaintextParams( $root->getLastRevision()->getContent( 'topic-title-plaintext' ) ); + if ( $isTopicTitleVisible ) { + $message->plaintextParams( $this->templating->getContent( $rootLastRevision, 'topic-title-plaintext' ) ); + } $message->params( $root->getWorkflow()->getOwnerTitle() ); // board title object diff --git a/includes/Templating.php b/includes/Templating.php index 4e76fe1..4270b28 100644 --- a/includes/Templating.php +++ b/includes/Templating.php @@ -127,7 +127,11 @@ class Templating { throw new InvalidInputException( 'Invalid format: ' . $format ); } - $allowed = $this->permissions->isAllowed( $revision, 'view' ); + $mainPermissionAction = ( $revision instanceof PostRevision && $revision->isTopicTitle() ) ? + 'view-topic-title' : + 'view'; + + $allowed = $this->permissions->isAllowed( $revision, $mainPermissionAction ); // Posts require view access to the topic title as well if ( $allowed && $revision instanceof PostRevision && !$revision->isTopicTitle() ) { $allowed = $this->permissions->isAllowed( -- 2.1.4