per SA-CORE-2009-007
User signatures (in comments) are potentially a XSS vulnerability if an admin changes the comment format.
D6 patch:
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.617.2.7
diff -u -p -r1.617.2.7 comment.module
--- modules/comment/comment.module 13 May 2009 17:15:10 -0000 1.617.2.7
+++ modules/comment/comment.module 1 Jul 2009 17:28:46 -0000
@@ -936,7 +936,7 @@ function comment_render($node, $cid = 0)
if ($cid && is_numeric($cid)) {
// Single comment view.
- $query = 'SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
+ $query = 'SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d';
$query_args = array($cid);
if (!user_access('administer comments')) {
$query .= ' AND c.status = %d';
@@ -957,7 +957,7 @@ function comment_render($node, $cid = 0)
else {
// Multiple comment view
$query_count = 'SELECT COUNT(*) FROM {comments} c WHERE c.nid = %d';
- $query = 'SELECT c.cid as cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.picture, u.data, c.thread, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.nid = %d';
+ $query = 'SELECT c.cid as cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data, c.thread, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.nid = %d';
$query_args = array($nid);
if (!user_access('administer comments')) {
@@ -1468,7 +1468,7 @@ function comment_form_add_preview($form,
$output = '';
if ($edit['pid']) {
- $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $edit['pid'], COMMENT_PUBLISHED));
+ $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $edit['pid'], COMMENT_PUBLISHED));
$comment = drupal_unpack($comment);
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$output .= theme('comment_view', $comment, $node);
@@ -1778,14 +1778,14 @@ function theme_comment_thread_expanded($
function theme_comment_post_forbidden($node) {
global $user;
static $authenticated_post_comments;
-
+
if (!$user->uid) {
if (!isset($authenticated_post_comments)) {
// We only output any link if we are certain, that users get permission
// to post comments by logging in. We also locally cache this information.
$authenticated_post_comments = array_key_exists(DRUPAL_AUTHENTICATED_RID, user_roles(TRUE, 'post comments') + user_roles(TRUE, 'post comments without approval'));
}
-
+
if ($authenticated_post_comments) {
// We cannot use drupal_get_destination() because these links
// sometimes appear on /node and taxonomy listing pages.
Index: modules/comment/comment.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.pages.inc,v
retrieving revision 1.2.2.1
diff -u -p -r1.2.2.1 comment.pages.inc
--- modules/comment/comment.pages.inc 7 Feb 2008 18:53:38 -0000 1.2.2.1
+++ modules/comment/comment.pages.inc 1 Jul 2009 17:28:46 -0000
@@ -70,7 +70,7 @@ function comment_reply($node, $pid = NUL
// $pid indicates that this is a reply to a comment.
if ($pid) {
// load the comment whose cid = $pid
- if ($comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $pid, COMMENT_PUBLISHED))) {
+ if ($comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $pid, COMMENT_PUBLISHED))) {
// If that comment exists, make sure that the current comment and the parent comment both
// belong to the same parent node.
if ($comment->nid != $node->nid) {
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.238.2.14
diff -u -p -r1.238.2.14 system.install
--- modules/system/system.install 27 Apr 2009 12:50:13 -0000 1.238.2.14
+++ modules/system/system.install 1 Jul 2009 18:36:09 -0000
@@ -2565,6 +2565,39 @@ function system_update_6050() {
}
/**
+ * Create a signature_format column.
+ */
+function system_update_6051() {
+ $ret = array();
+
+ if (!db_column_exists('users', 'signature_format')) {
+
+ // Set future input formats to FILTER_FORMAT_DEFAULT to ensure a safe default
+ // when incompatible modules insert into the users table. An actual format
+ // will be assigned when users save their signature.
+
+ $schema = array(
+ 'type' => 'int',
+ 'size' => 'small',
+ 'not null' => TRUE,
+ 'default' => FILTER_FORMAT_DEFAULT,
+ 'description' => 'The {filter_formats}.format of the signature.',
+ );
+
+ db_add_field($ret, 'users', 'signature_format', $schema);
+
+ // Set the format of existing signatures to the current default input format.
+ if ($current_default_filter = variable_get('filter_default_format', 0)) {
+ $ret[] = update_sql("UPDATE {users} SET signature_format = ". $current_default_filter);
+ }
+
+ drupal_set_message("User signatures no longer inherit comment input formats. Each user's signature now has its own associated format that can be selected on the user's account page. Existing signatures have been set to your site's default input format.");
+ }
+
+ return $ret;
+}
+
+/**
* @} End of "defgroup updates-6.x-extra"
* The next series of updates should start at 7000.
*/
Index: modules/user/user.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.install,v
retrieving revision 1.5.2.1
diff -u -p -r1.5.2.1 user.install
--- modules/user/user.install 6 Jan 2009 15:46:38 -0000 1.5.2.1
+++ modules/user/user.install 1 Jul 2009 17:28:46 -0000
@@ -191,6 +191,13 @@ function user_schema() {
'default' => '',
'description' => "User's signature.",
),
+ 'signature_format' => array(
+ 'type' => 'int',
+ 'size' => 'small',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {filter_formats}.format of the signature.',
+ ),
'created' => array(
'type' => 'int',
'not null' => TRUE,
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.892.2.13
diff -u -p -r1.892.2.13 user.module
--- modules/user/user.module 27 Apr 2009 12:02:27 -0000 1.892.2.13
+++ modules/user/user.module 1 Jul 2009 17:37:16 -0000
@@ -532,7 +532,7 @@ function user_fields() {
}
else {
// Make sure we return the default fields at least.
- $fields = array('uid', 'name', 'pass', 'mail', 'picture', 'mode', 'sort', 'threshold', 'theme', 'signature', 'created', 'access', 'login', 'status', 'timezone', 'language', 'init', 'data');
+ $fields = array('uid', 'name', 'pass', 'mail', 'picture', 'mode', 'sort', 'threshold', 'theme', 'signature', 'signature_format', 'created', 'access', 'login', 'status', 'timezone', 'language', 'init', 'data');
}
}
@@ -1519,6 +1519,15 @@ function user_edit_form(&$form_state, $u
'#default_value' => $edit['signature'],
'#description' => t('Your signature will be publicly displayed at the end of your comments.'),
);
+
+ // Prevent a "validation error" message when the user attempts to save with a default value they
+ // do not have access to.
+ if (!filter_access($edit['signature_format']) && empty($_POST)) {
+ drupal_set_message(t("The signature input format has been set to a format you don't have access to. It will be changed to a format you have access to when you save this page."));
+ $edit['signature_format'] = FILTER_FORMAT_DEFAULT;
+ }
+
+ $form['signature_settings']['signature_format'] = filter_form($edit['signature_format'], NULL, array('signature_format'));
}
// Picture/avatar:
@@ -2031,7 +2040,7 @@ function user_comment(&$comment, $op) {
// Validate signature.
if ($op == 'view') {
if (variable_get('user_signatures', 0) && !empty($comment->signature)) {
- $comment->signature = check_markup($comment->signature, $comment->format);
+ $comment->signature = check_markup($comment->signature, $comment->signature_format, FALSE);
}
else {
$comment->signature = '';
Comments
Comment #1
berdirI was wondering why the update function does use 0 as the default value. 0 means it will always use the format that is the default when displaying the signature while 1/filter_resolve_format() would stick to the format that was the default when the update has been executed.
I'm not saying it is wrong, I'm just interested in the reason as we are doing something similiar (adding format column in a update function) in privatemsg.module.
Comment #2
webchickCould someone please take a look at this so we can ship alpha 1 with no known security holes? :)
Comment #3
berdirLet's try this, I suppose we don't need an upgrade function and someone from the security team needs to look over this. I have *not* tested the patch.
What confuses me a bit is that we have a http://api.drupal.org/api/function/theme_user_signature/7 but I can't see where we would be using that one.
Additionally, the whole logic is a bit strange, since we load the signature and signature_format hardcoded in comment.module, but we call check_markup on it in user_comment_view().
Comment #5
berdirOoopsie. Added a missing ,.
Comment #7
berdirReplaced default filter format constant with function...
Comment #9
berdirTwo more fixes:
- load filter format before using filter_access
- I'm not sure why, but in some cases, $account->signature_format is unset. Added an additional check.
Comment #10
heine commentedFor people upgrading from earlier versions of 6.x we need to add an update function.
Comment #11
berdirDon't we require that people upgrade to the latest version of D6 before going to D7? Just wondering, that's what I thought.
Comment #12
coltraneI believe we're referring to input formats as text formats in the UI now.
Edit: Also, the last sentence isn't quite clear, "It will be changed to a format you have access to when you save this page." I don't have a recommendation yet though.
Comment #13
David_Rothstein commentedSubscribing.
Note that http://api.drupal.org/api/function/system_update_6051/7 is still sitting in the D7 codebase (but with D6 code living in it), and tries to do the same update as the function here.
So it seems this should either be removed (or fixed) as part of this patch?
Comment #14
webchickComment #15
David_Rothstein commentedAlso, http://api.drupal.org/api/function/filter_update_7005/7 is relevant here. It seems we'd need to do the same kind of update on the {users} table as that function is already doing on other core tables that store text formats in them.
Comment #16
David_Rothstein commentedCrosspost.
Comment #17
bleen commentedtests for signatures have been started at: #276582: Tests needed: user.module
Comment #18
sun.core commented.
Comment #19
sunFixing component.
Comment #20
catch#760014: Stop supporting direct updates from Drupal < 6.17 would remove system_update_6050() from D7, and require upgrading from a supported version of D6 before making the move to D7.
This will need updating for the #text_format type.
Comment #21
sunUpdated, cleaned up, and whatnot. Reviews welcome.
Comment #22
catchLooks straightforward, we don't technically need the update once the issue from #20 is in but it doesn't do any harm either. If we have any tests for user signatures at all then that's probably sufficient but don't have time to look right now.
Comment #23
sun>90% of this patch doesn't come from me as I merely re-rolled and updated the code. Since it's D6 where this module/security update ultimately happened, and this patch merely applies the same changes, I'm calling this RTBC.
Comment #24
catchAgreed.
Comment #25
David_Rothstein commentedShould this be NULL instead of filter_default_format() - seems to be what other places are using?
I'm not sure I understand the @todo - is this referring to the idea that $comment->signature could become a real field someday? I think the @todo should either be removed or expanded upon...
Comment #26
sunNot sure I understood your issue with http://api.drupal.org/api/function/filter_update_7005/7 and http://api.drupal.org/api/function/system_update_6051/6 -- effectively, I'd almost rather say that we can drop the module update entirely. If the D6 update needs to be corrected in any way, then that sounds like a separate issue (and too late?).
Therefore, removed that module update.
Also, I reconsidered and realized that the logic was not correct: It only took into account an inaccessible format, but not an invalid format. Furthermore, it tested the permissions of the current user, not the permissions of the user account that is edited.
Lastly, User module doesn't implement hook_filter_format_delete() yet, so deleting an text format leads to the same issue all over again. Since D6 does not provide this hook, the assigned format of existing user signatures after upgrading can still be invalid or inaccessible. The new logic accounts for all of that, and this also means that this code likely as to stay... longer. (I'm still searching someone to discuss and shape a total filter system revamp for D8+...)
Also clarified that @todo in user_comment_view().
Comment #27
David_Rothstein commentedThe update function was OK for Drupal 6, but we need to correct it for D7 since '0' (FILTER_FORMAT_DEFAULT) is no longer interpreted as anything by Drupal. I think you are right that dropping the module update entirely from the D7 codebase is OK, but we still need to update D6 sites to ensure that the actual format is always stored, rather than '0'.
See the code for the {block_custom} table in http://api.drupal.org/api/function/filter_update_7005/7 -- we essentially need to do the same thing for the {users} table here in the user.module... and hm, come to think of it, that code for {block_custom} should probably be in the block module, shouldn't it :)
Comment #29
sunBy updating the values, the user won't be notified that he needs to update the signature.
Comment #30
sunWTF? Why does this pass at all?
This means we have zero tests for user signatures.
Comment #31
sunThis should fail.
Comment #33
sunGood.
But. WTF is that?
Comment #34
sunWell, I can see that User module even contains a conditional database schema-based constraint + validation on the signature form value, so as to not exceed the 255 chars. I have no idea why this was done, but it seems the intention was to allow a contrib module to replace the schema column type...
So, I think this patch is ready to fly.
@David: Not sure whether my reply above was understandable -- if we update any values on top of the updates that already ran for D6, then no user will get the message that he needs to update the signature. A signature_format value of 0 will reset the format to NULL and therefore preselect the user's default format, but still needs updating anyway. Therefore I think we do not have to update anything additionally.
Comment #35
David_Rothstein commentedHm, OK, I see that, but on the other hand, if anyone ever views that user's signature before the user has gone to their account page and reset it, then that will result in a call to check_markup() with "0" passed in as the format ID, which is basically undefined behavior in Drupal 7 right now.
I guess #562694: Text formats should throw an error and refuse to process if a plugin goes missing is the issue that deals with what to do in that undefined situation, but in general I think the consensus is we never want to intentionally pass "0" in for this because it doesn't really mean anything in Drupal 7 (and never really had an intelligent meaning before D7, although at least it had a defined one). For example, your proposal in #562694: Text formats should throw an error and refuse to process if a plugin goes missing involves returning an empty string in this case, which would basically mean that user's signature will never be displayed until the next time they bother to save their user account info (which might be never).
I don't think we want to treat this differently than any of the other formats in core - for all the others we have a database update to replace the undefined "0" format with the actual sitewide default format at the moment of the D6->D7 upgrade, so it seems like we should do the same thing here...
Comment #36
jbrauer commentedStandardizing security tags.
Comment #37
pwolanin commentedComment #38
yesct commentedsounds like David said in #35 that this needs work to remove the 0 and do it The Right Way. (my guess at summarizing this).
Comment #39
duellj commentedThe text_format field has changed the way it stores value/format in $form_values: #735662: hook_form_alter() can easily clobber a text format
Updated the patch from #33 to reflect the changes.
Comment #40
yesct commented#39: drupal.user-signature-format.39.patch queued for re-testing.
Comment #41
catchThe Drupal 6 upgrade does this:
We also decided to stop supporting updates from before this was fixed in Drupal 6, see #760014: Stop supporting direct updates from Drupal < 6.17.
So, we should remove the drupal_set_message() hunk from here, and update filter_update_7005() to include the {users}.signature_format column.
Comment #42
catchComment #43
catchUntested re-roll with that change.
Comment #44
sunComment #45
dries commentedIs this something that mere mortals can make sense of? I don't think my mother would be able to understand what an 'input format' is.
Comment #46
dries commentedBy the way, I looked at this patch and I'm happy with it. It is an important patch to get in so my comment in #45 is not a show-stopper. I'll give this a bit more time for discussion, if not, I'll commit the patch as is.
Comment #47
bleen commentedRe #45 ...How bout:
"You do not have permission to save your signature in that format. A different format will be used when you save this page."
Dries's mom??
Comment #48
David_Rothstein commentedUh, what patch are you all looking at?
That message about input formats is D6 code that has been in D6 for a while, not D7. The patch in #43 doesn't have a drupal_set_message() in it at all.
Comment #49
David_Rothstein commentedThis is never used in the patch.
This is outdated - we no longer have that message in the patch so shouldn't be looking for it in the test.
Other than that, everything looks good.
Comment #50
Steven Merrill commentedNew patch based on #43 that actually tests the page content on a user's comment to be sure that HTML is stripped out when the user's signature format is set to plain text.
Comment #51
Steven Merrill commentedUsing assertRaw() and assertNoRaw(), and some cleaner test messages suggested by chx.
Comment #52
David_Rothstein commentedNice!
Let's make it even a tiny bit more robust by having the administrator edit the comment after it's created to change the comment itself to Full HTML. That way we direct test the security issue by making sure that the <h1> tags in the signature don't get displayed even when the comment itself has a text format that would allow them.
I also removed the
$this->assertNoText(t('Your signature needs to be updated.')line, as per above.Comment #53
chx commentedIs this the last beta blocker, I wonder.
Comment #54
webchickLooks great, fixes a critical bug, a critical security bug, and adds tests for user signatures too! Bonus! :D
I was terribly confused by this sentence:
" Consider to use $comment->signature_safe or similar here
+ // and elsewhere."
After consulting with chx, I changed it to:
"Consider renaming to $comment->signature_safe or similar
// here and elsewhere in Drupal 8."
and... committed to HEAD! Thanks! :D
Comment #55
David_Rothstein commentedLooks like this wasn't actually committed?
Comment #56
David_Rothstein commentedBecause of that, here's a reroll with the change from #54 included as part of the patch.
Comment #57
dries commentedCommitted to CVS HEAD. Thanks all.
I'm also moving this one to the 'HEAD to HEAD' project.
Comment #58
sunWhy did we remove that text format check and warning message on user profile view? The bogus text format situation can technically happen anytime...? The innocent site member/user won't have any clue that the user profile needs to be updated to make the user signature re-appear properly formatted!
Comment #59
David_Rothstein commentedThe patch contained an update function fix to ensure that all signatures updated from Drupal 6 had an actual format assigned, so there shouldn't be any legitimate way to get a bogus text format (or at least no more legitimate than any other format selector in Drupal, and the general case is being handled elsewhere).
The code that was removed looked like this:
A format that the user does not have access to (the second condition) definitely can happen legitimately if the administrator edits the user's signature, but that just gets handled the standard way now (they get a message in the textarea saying that they don't have permission to edit it), right? Is there a reason signatures should be handled differently from everything else?
Comment #60
David_Rothstein commentedBy the way, Head2Head issue is here: #824932: Update for user signature_format
Comment #61
rfayPlease explain to me why we don't do hook_update_N() to add columns in cases like this. I understand why Head2Head is required in more complex manipulations, but I just don't understand why we don't add the hook_update_N() here.
Comment #62
webchickrfay: Basically, we are trying to get beta 1 out the door as quickly as possible. In order for that to happen, the upgrade path has to work. We were wasting a whole bunch of time trying to support updating from any old arbitrary D6 version to D7, and made a judgment call that from now on, we'd only support upgrading from the latest D6 version (atm, 6.17), which is consistent with what D5 and D6 did as well. This update already exists in the latest version of D6, so is therefore not needed in D7.
The good news is that once beta 1 comes out, we won't need a HEAD to HEAD project, because D7 will support HEAD to HEAD upgrades from then on. Or so is the plan, anyway.
Comment #63
sunok, let's see whether we can omit that message. Could basically form-altered in through a contrib/custom module even, if I'm not mistaken.
@rfay: This patch did not contain an update, because the update was already added, committed, and released for Drupal 6, so all existing sites should already have it.
Off topic, but important: It's too early for beta, we still have major API changing issues in the queue, which cannot be solved without API changes. Let's not call something beta just for the sake of the word beta, please.
Comment #64
webchicksun: Add 'em to http://drupal.org/community-initiatives/drupal-core.
Comment #66
webservant316 commentedsubscribe