Due to recent security issues with modules trying to provide live previews of content, I thought it might be useful if we provide a core jQuery method that can run text through Drupal's input filters. We provide a jQuery-version of check_plain(), why not check_markup() as well? This way modules that need this kind of functionality aren't trying to create it themselves and doing it insecurely.

Comments

dave reid’s picture

litwol’s picture

Version: 7.x-dev » 8.x-dev

Including myself in this initiative.

Setting this to 8x-dev as i really doubt this has a chance at 7.

dave reid’s picture

Version: 8.x-dev » 7.x-dev
Status: Active » Needs review
StatusFileSize
new2.34 KB

Initial patch for review. Reinforces the fact that I suck at jQuery because I can get the result in the success callback but can't get it to return a proper value.

dave reid’s picture

Although I didn't mean to cross post, I want to make a case for this to be accepted into Drupal 7's API slush. I'd like our Drupal 7 modules to be able to use this function and not keep coding their own, insecure versions.

dave reid’s picture

StatusFileSize
new3.22 KB

woot! Working version now with async: false.

dave reid’s picture

StatusFileSize
new2.32 KB

Named the JS function wrong.

dave reid’s picture

This could really use a look-over by a jQuery ninja.

ufku’s picture

The Ajax Markup API was created for this purpose.
I use it with BUEditor. I know there is the live module doing a similar thing but it is limited to node and comment preview and it previews the whole node.
Ajax markup, OTH, is just an ajaxified version of check_markup.

@Dave, instead of returning the output you should consider taking the success or complete callback as a parameter of Drupal.checkFormat

dave reid’s picture

Adding a callback to Drupal.checkFormat didn't seem consistent with the way core handles these types of methods. Also fixing cross-post.

ufku’s picture

I've just seen #586180.

ufku’s picture

Sorry, I'm not able to post patches right now.
"async: false" is not a good choice as it freezes the browser. A loading notice is always better than a locked browser interface.

dave reid’s picture

Yeah that's why I need a jQuery ninja to help me out. I couldn't get it to return the value unless I specified async: false.

john morahan’s picture

ufku’s picture

url = Drupal.settings.basePath + 'js/filter/check-markup';
This will not produce the right url when the site is multilingual and uses an url prefix.

Current implementation is vulnerable to CSRF as it does not set a token and check it in $_POST

To solve these we should include an url and a token in each page.

markus_petrux’s picture

subscribing

kkaefer’s picture

Status: Needs review » Needs work

Using SYNCHRONOUS XMLHttpRequests completely blocks the browser when performing those requests. That means the user can’t scroll, click, type or in fact do anything with the website. Add a suboptimal internet connection and a badly tuned server to the mix and you end up with a user experience that is below zero.

dave reid’s picture

I'm completely aware that I should be avoiding the async: false option, but like I said before, I need a jQuery ninja to help figure out how to properly get the return value without async: false. I couldn't get it to work unless I did that.

dave reid’s picture

@ufku: Did you actually test it? Yes the request will work just fine. That's how we format URLs for our Drupal AJAX requests. Please also explain how it would be vulnerable to an CSRF attack since we don't include any kind of token-type valiation to auto-completion or Drupal.checkPlain()?

john morahan’s picture

well, checkPlain and autocomplete do not have any side effects. check_markup could for example invoke the php filter.

markus_petrux’s picture

To work asynchronously, this needs a callback that can be invoked when the AJAX request succeeds. This pattern can be found in ajax.js (D7) or ahah.js (D6).

markus_petrux’s picture

In fact, it would be nice if this feature could reuse ajax.js stuff, I think. I haven't been following D7 updates on this, but looking at the code, it seems it could be possible.

kkaefer’s picture

We should probably use something like this:

$.fn.checkMarkup = function (input, format, langcode, cache, callback) {
  return this.load(
    Drupal.settings.basePath + 'js/filter/check-markup',
    jQuery.param({
      input : input,
      format : format || false,
      langcode : langcode || '',
      cache : cache || true
    }),
    callback
  );
};

This approach uses the jQuery.load() function to change the markup of a DOM element. The way you use it is as follows: $('#foo').checkMarkup($('textarea').val()). This doesn’t return the checked value but instead replaces #foo’s contents with the return value. That’s how asynchronous callbacks work. In case you really need the return value, you can create an empty DOM node, call this function and pass a callback function that will be called when the DOM node contains the result:

$('<div>').checkMarkup(..., null, null, null, function () {
  /* $(this).html() is the return value */
});

(the code is untested)

pwolanin’s picture

Status: Needs work » Needs review
Issue tags: -Needs JavaScript review +Security Advisory follow-up

Seems like there needs to be some kind of additional access checking here? - imagine that anonymous users cannot post any content. An attacker could still post content to this URL and possibly exploit filter system bugs or cause high load.

The access callback could invoke a hook to see if the current user has permission to use JS-based filtering?

pwolanin’s picture

Status: Needs review » Needs work
Issue tags: -Security Advisory follow-up +Needs JavaScript review

cross-post

dave reid’s picture

That's tough because it should be available to anyone who has a change to make any kind of input that uses the filter system. That would be nodes, blocks, comments, fields, etc. Not sure what the best way to check such a broad access would be.

pwolanin’s picture

@Dave Reid - no only to users who have access to a JS front-end to make those changes. I..e. if you can access tinyMCE, then the module providing it should arrange for the access callback to trturn true (e.g. via hook invocation)

ufku’s picture

@ufku: Did you actually test it? Yes the request will work just fine. That's how we format URLs for our Drupal AJAX requests.

That's not the case for multilingual paths. The output of /somepath is not always the same as /LANG_CODE/somepath.
Drupal.settings.basePath should be used for producing file paths but not for producing URLs. Instead an URL produced by the url() function can be added to Drupal.settings.
Besides, the current implementation assumes clean-urls is on.

Please also explain how it would be vulnerable to an CSRF attack since we don't include any kind of token-type valiation to auto-completion or Drupal.checkPlain()?

As Morahan said, it could be possible to execute PHP with POST data sent remotely by(in behalf of) an admin.

dave reid’s picture

People who have access to the PHP filter are the only ones that would be able to execute PHP code, so there's not really too much risk there. The main thing is if this were available to anonymous users and used to try and bot-hack sites in mass.

ufku’s picture

What I meant was, an admin may enter a malicious site which silently posts a form containing the PHP code to the admin's site where he is still logged in. The admin wouldn't even know that he/she has posted and executed the code. It's definitely high risk.

Against high load, +1 for a 'use ajax filtering' kind of permission.

kkaefer’s picture

ufku, you’re right. This kind of attack is called CSRF and it can be mitigated/solved by using the form tokens.

pwolanin’s picture

Interesting point - so given the potential that this would include the PHP filter, the POST needs to reference a form ID and token or ... ?

How does the current live module handle this?

ufku’s picture

Actually the Ajax Markup API covers nearly all of the above issues.
But, I'm not sure a similar implementation may have a chance to go into D7.

dave reid’s picture

Version: 7.x-dev » 8.x-dev

Moving to 8.x

David_Rothstein’s picture

Version: 8.x-dev » 7.x-dev

I think as a security hardening patch and a relatively small change, this still would have a decent shot at being accepted for D7.

However, it's not something that I personally have time or skill to work on anytime soon... so mostly just subscribing :)

sun’s picture

Version: 7.x-dev » 8.x-dev
Issue tags: -input formats

No longer possible to do for D7.

mgifford’s picture

Status: Needs work » Needs review
Issue tags: -jQuery, -Security improvements, -JavaScript, -Security, -Needs JavaScript review

#6: 586098-jquery-checkmarkup-D7.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +jQuery, +Security improvements, +JavaScript, +Security, +Needs JavaScript review

The last submitted patch, 586098-jquery-checkmarkup-D7.patch, failed testing.

nod_’s picture

Assigned: dave reid » Unassigned
Issue tags: -Needs JavaScript review

if there is an ajax call involved Drupal.checkMarkup has to have a callback parameter that will be fired once the data comes back.

Also since nothing special is going on with the parameters, it should accept an object directly passed to the $.ajax call.

Drupal.checkMarkup(data, callback);

nod_’s picture

Title: Add a core jQuery checkMarkup() function like check_markup() » Add a core Drupal.checkMarkup() function like check_markup()
sun’s picture

+    async: false,

The idea here is to use a synchronous/blocking request.

AFAICS, it is likely that code, which does or will actually use this functionality will have a full stack of preceding and/or subsequent/following logic that depends on the outcome of the filtered user input.

For now, I'd recommend to continue with the synchronous/blocking request approach. A later version could possibly allow to extend/override the default Ajax options for special use-cases.

nod_’s picture

This is bad bad bad. On mobile? deadly.

The function is not used anywhere since it doesn't exists. It should be in it's documentation that the way to use it is explained, properly, with a callback.

Please don't introduce bad practices.

And just for example: http://bugs.jquery.com/ticket/8819

mgifford’s picture

Issue summary: View changes

@nod_ so you want to mark this as Closed, Won't fix?

nod_’s picture

Status: Needs work » Closed (won't fix)

yup.