Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

prevent third-party requests from using fetch #203

Merged
merged 7 commits into from

6 participants

@Chris--S
Collaborator

this patch is pulled out of fetchissues2 branch to separate it from the (much more complex) image version limiting code

@Chris--S Chris--S merged commit 892345c into from
@Chris--S Chris--S deleted the branch
@HakanS
Collaborator

Some plugins using fetch.php

C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\abc\abc\syntax.php (2 hits)
    Line 381:         $abcMediaUrl=DOKU_BASE."lib/exe/fetch.php?media=".$this->getConf('mediaNS').":";
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\applet\applet\code.php (1 hits)
    Line 87:       $archive = DOKU_BASE . 'lib/exe/fetch.php?media=' . $archive;
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\chain\chain\syntax.php (1 hits)
    Line 315:             $renderer->doc .= '<img src='.DOKU_BASE.'lib/exe/fetch.php?media='.$OutFileNamePng.' alt="Png Image Chain Plugin Error" />';
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\dlcount\dlcount\action.php (6 hits)
    Line 59:             if (strpos($href, 'fetch.php?') !== false) {
    Line 63:             } elseif (strpos($href, 'fetch.php/') !== false) {
    Line 66:                 $fn = '/' . substr($href, strpos($href, 'fetch.php/')+strlen('fetch.php/'));
    Line 66:                 $fn = '/' . substr($href, strpos($href, 'fetch.php/')+strlen('fetch.php/'));
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\draw\draw\syntax.php (1 hits)
    Line 81:         $fetchpath = $conf['baseurl']."/lib/exe/fetch.php?media=$namespace";
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\farm\farm\animal.class.php (2 hits)
    Line 85:                }else $url .= 'lib/exe/fetch.php?w=&l=&cache=cache&media='.$id;
    Line 97:                }else $url .= 'lib/exe/fetch.php?w=&l=&cache=cache&media='.$id;
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\fckglite\fckg\action\edit.php (3 hits)
    Line 1104:                     else if(attrs[i].escaped.match(/exe\/fetch.php/)) {
    Line 1335:                                 // fetched by fetch.php
    Line 1357:                      else if(matches = attrs[i].escaped.match(/\/lib\/exe\/fetch.php\/(.*)/)) {
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\freesync\freesync\helper.php (1 hits)
    Line 387:               $fetchurl = str_replace('xmlrpc.php', 'fetch.php?media='.$id, $this->_profile['xmlrpcurl']);
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\godiag\godiag\syntax.php (2 hits)
    Line 137:         $sgf_href = DOKU_BASE.'lib/plugins/godiag/fetch.php?f='.$data['md5hash_sgf'].'&amp;t=sgf';
    Line 138:         $png_href = DOKU_BASE.'lib/plugins/godiag/fetch.php?f='.$data['md5hash_png'].'&amp;t=png';
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\isbn\isbn\code.php (1 hits)
    Line 83:             $src     = DOKU_BASE.'lib/exe/fetch.php?media='.urlencode($imglink);
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\jdraw\jdraw\syntax.php (1 hits)
    Line 85:         $fetchpath = DOKU_BASE."lib/exe/fetch.php?media=$namespace";        
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\latex\latex\latexinc.php (1 hits)
    Line 46:                        DOKU_BASE.'lib/exe/fetch.php?media='.$this->getConf('latex_namespace').':',
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\lightbox\lightbox\syntax.php (1 hits)
    Line 102:           $src = "/wiki/lib/exe/fetch.php?media=$src";
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\lytebox\lytebox\action.php (2 hits)
    Line 80:       else if (strpos(strtolower($matches[1]),"/lib/exe/fetch.php")) $type="ext";
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\math\mathmulti\syntax.php (1 hits)
    Line 64:   $mathmultiplugin_urlimg = DOKU_URL.'lib/exe/fetch.php?w=&amp;h=&amp;cache=cache&amp;media=';
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\math2\math\syntax.php (1 hits)
    Line 37:   $mathplugin_urlimg = DOKU_URL.'lib/exe/fetch.php?w=&amp;h=&amp;cache=cache&amp;media=';
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\mimetex\mimetex\syntax.php (1 hits)
    Line 90:         $cacheurl = DOKU_BASE.'lib/exe/fetch.php?media='.urlencode('latex:'.$hash.'.gif');
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\mp3play\mp3play\syntax.php (1 hits)
    Line 117:             $renderer->doc .= '    <param name="FlashVars" value="' . $params . 'soundFile=' . DOKU_URL . '/lib/exe/fetch.php?media=' . $data['mp3'] . '" />' . DOKU_LF;
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\mp3play2\mp3play\syntax.php (1 hits)
    Line 123:           $data = $url.'/lib/exe/fetch.php/'.$data;
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\multilingual\multilingual\syntax.php (2 hits)
    Line 99:    $out .= '              <span class="curid"><a href="/doku/doku.php/en:doku_doodles" class="media" title="en:doku_doodles"><img src="/doku/lib/exe/fetch.php?w=&amp;h=&amp;cache=&amp;media=http%3A%2F%2Fsnorriheim.dnsdojo.com%2Fdoku%2Flib%2Fplugins%2Fmultilingual%2Fflags%2Fgb.gif" class="media" title="English" alt="English" /></a></span>'.NL;
    Line 104:   $out .= '              <a href="/doku/doku.php/ko:doku_doodles" class="media" title="ko:doku_doodles"><img src="/doku/lib/exe/fetch.php?w=&amp;h=&amp;cache=&amp;media=http%3A%2F%2Fsnorriheim.dnsdojo.com%2Fdoku%2Flib%2Fplugins%2Fmultilingual%2Fflags%2Fkr.gif" class="media" title="??? (Korean)" alt="??? (Korean)" /></a>'.NL;
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\newsboard\newsboard\syntax.php (1 hits)
    Line 128:               $image =" <input type='image' border='0' src='/dokuwiki/lib/exe/fetch.php?media=".$media."' height='100' width='100'  align='left' alt=''/>"; 
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\orphanmedia\orphanmedia\syntax.php (2 hits)
    Line 381:                               . '"><img src="'. DOKU_URL . 'lib/exe/fetch.php?media=' . $rt2 
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\panorama\panorama\render_helper.php (2 hits)
    Line 25:         $archive = DOKU_BASE.'lib/exe/fetch.php?media='.$archive;
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\pchart\pchart\code.php (1 hits)
    Line 105:         $html_code = '<a href="/lib/exe/detail.php?id='.getID().'&amp;cache=cache&amp;media='.$chartWikiFullPath.'" class="media" title="'.$name.'"><img src="/lib/exe/fetch.php?h=&amp;cache=cache&amp;media='.$chartWikiFullPath.'" class="media';
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\pdfex\pdfex\action.php (1 hits)
    Line 244:     $repl_searchfor = '/src="' . preg_quote(DOKU_INC, '/') . 'lib\/exe\/fetch.php?(.*?)media=(.+?)"/';
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\register\register\syntax.php (1 hits)
    Line 68:        $url = DOKU_BASE . "lib/exe/fetch.php?cache=$cache&amp;media=" . urlencode("register:$hash.png");
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\siteexport\siteexport\action\ajax.php (4 hits)
    Line 748:                 $url = str_replace('detail.php', 'fetch.php', $url);
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\svg\svg\syntax.php (1 hits)
    Line 34:             'url'    => 'http://www.cvpcs.org/dokuwiki/lib/exe/fetch.php?id=project%3Adokuwiki-svg&cache=cache&media=project:dokuwiki-svg.tar.gz',
  C:\DokuWikiStickNew\dokudevel\tmp2011\plugins\visio\visio\syntax.php (2 hits)
    Line 82:           $renderer->doc .= DOKU_URL."lib/exe/fetch.php?id=".$data['file']."&cache=cache&media=".$data['file'];
    Line 88:           $renderer->doc .= '<a href="'.DOKU_URL.'lib/exe/fetch.php?id='.$data['file'].'&cache=cache&media='.$data['file'].'" class="media mediafile mf_vsd" title="'.$data['file'].'">'.$data['file'].'</a><br/>';
@Chris--S
Collaborator

Notes on plugins:

  • ABC: ok, doesnt' use w,h
  • APPLET: ok, not images
  • CHAIN: ok, doesn't use w,h
  • DLCOUNT: ok, modifies an existing fetch url
  • DRAW: ok, doesn't use w,h
  • FARM: ok, w,h used but empty.
  • FCKGLITE: maybe incompatible. I didn't see w,h parameters, but purpose looks like they could be there
  • FREESYNC: ok, doesn't use w,h (if it does use images)
  • GODIAG: ok, fetch reference is its own fetch.
  • ISBN: ok, remote image request without w,h.
  • JDRAW: ok, doesn't use w,h
  • LATEX: maybe ok, doesn't appear to use w,h - further checking required.
  • LIGHTBOX: unknown. couldn't access plugin source
  • LYTEBOX: ok, doesn't generate its own fetch url, does replacements on existing fetch url
  • MATH: ok, uses w,h but empty
  • MATH2: ok, doesn't use fetch any more. code above uses w,h but empty so still should be ok.
  • MIMETEX: ok, doesn't use w,h.
  • MP3PLAY: ok, not images
  • MP3PLAY2: ok, not images
  • MULTILINGUAL: ok, fetch references are commented out
  • NEWSBOARD: ok, doesn't use w,h
  • ORPHANMEDIA: incompatible, requires fixing.
  • PANORAMA: ok, not bitmap images
  • PCHART: ok, uses h, but empty
  • PDFEX: should be ok, appears to do pattern based replacing on an existing fetch url
  • REGISTER: ok, doesn't use w,h
  • SITEEXPORT: should be ok, seems to modify existing fetch url
  • SVG: ok, not an image.
  • VISIO: ok, not an bitmap image, w,h not in url.
@sergstetsuk

good patch but it breaks my plugin "rotate" which loads couple of images with javascript. Tokens couldn't be computed on client side. So all images loaded by JS aren't available. Plugin doesn't work with latest version of dokuwiki.

Collaborator

Neat plugin.

I have a couple of possible solutions.
1. plugin pre-calculate tokens for all possible images and send them with the initial image.
2. add an event to the token check allowing a plugin to modify the parameters or the result.

I'm open to other ideas, preferences or comments.

Collaborator

That's not feasible. The token is there to mitigate against a DDOS attack using fetch queries to trigger image resizes as the attack vector. With no token, such an attack would work against internal images. The below quote from an email I authored explains more

"This all started after I came across this drupal security notice, http://drupal.org/SA-CORE-2013-002 (which they rate as critical), and went to check if the same attacks work against fetch.

[snip .. referring to #200]

Drupal's solution is to add a token to the resize request. That will mitigate against using third party tools to generate the fetch requests, it wouldn't be successful against adding a page(s) to a wiki containing lots of cropped images.

Possibly we could implement a count against the number of resizes used for an image, once its above a certain limit, purge all out of date resizes/crops and if its still above the limit, use the native image."

also see #202

@glensc

this salting is susceptible to length and timing attacks

http://happybearsoftware.com/you-are-dangerously-bad-at-cryptography.html

and if i understand it correctly, it would be even possible to guess what your auth_cookiesalt() is with those techiques

Care to elaborate how, exactly?

The article states "if you know the value of md5('secretfoo:bar'), it's trivial to compute md5(secretfoo:bar&bar:baz) without knowing the prefix 'secret'."

  • if that's true, an attacker could work around not knowing 'secret' but would never learn the 'secret' itself
  • unfortunately the article does not explain how exactly you would compute the second MD5 without knowing secret
  • it also does not explain what "trivial" means is trivial a matter of seconds? hours? days?

Finally a simple fix might be to put the secret at the end (if the article is right).

i fall to the Conscious incompetence category :) and as i understand moving it to the end you are vulnerable to a collision attack and moving message to be in center is bad as well (the same link explains it).

so really you should use hmac-md5 there:

$hash = substr(hash_hmac('md5', $data,  auth_cookiesalt()), 0, 6);

if dependency on hash extension is not wanted, there are plenty of php-only implementations out there

BTW. I think we're not vulnerable here, because we shorten the MD5. Thus the full hash result is not known to the attacker and can not be used for a continuation calculation needed for a length attack

Collaborator

I've just read through the blog article and the linked exploit description for flickr. In my opinion we are not vulnerable for several reasons.

  1. As @splitbrain has already noted, we shorten the MD5. This means that you don't know the full internal state so you can't continue the hash calculation.
  2. I don't believe that you can do timing attacks on string comparisons of 6 characters that are calculated in PHP behind a standard web server. Even if you can avoid network latencies and get down to measuring in µs of execution time difference I doubt that this works for dynamic languages like PHP (I remember having problems to get reliable time measurements for the execution of a local Java program...). If this timing attack would work you would need 6 * 16 = 96 time calculations in order to guess a whole token, if you need some thousand requests in order to get reliable timings you get easily in the region of a million requests in order to get a single token. As there are only 16777216 different tokens I think simply doing a brute force attack against the real token value is a lot more effective. Furthermore for these tokens for images you actually need to calculate a lot of tokens in order to cause any harm. We are using similar tokens for links but I also don't see any problems there.
  3. Concerning length extension attacks: Hash functions that are based on the Merkle-Damgård construction (e.g. md5) are splitting the input into blocks and compute the hash with a function that takes an old internal state and one input block and then output a new internal state. The last internal state is the hash value, the first internal state is fixed. However there is a problem even if you know the complete internal state: The input is always padded with a one bit, some zero bits in order to get to full blocks and the message length (32-bit little-endian). This means that even though you can add that padding this won't help as I'm relatively sure that our integer parsing will strip the padding and everything that is appended after it as the first bit of the padding is "1" and thus a non-ASCII character so you can't change the image size. These length extension attacks are - if you know the length of the secret, otherwise you need to guess - as fast as calculating a normal md5sum.

I think it would still be better to use HMAC in the places where want to have a message authentication code but imo there is no need to hurry as in all places where the full md5sum is used as token are those where the user name is the last part in the hash calculation and in the case of user names length extension attacks shouldn't work as the padding will make the user name invalid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
19 _test/core/TestRequest.php
@@ -18,6 +18,9 @@ function ob_start_callback($buffer) {
*/
class TestRequest {
+ private $valid_scripts = array('/doku.php', '/lib/exe/fetch.php', '/lib/exe/detail.php');
+ private $script;
+
private $server = array();
private $session = array();
private $get = array();
@@ -27,6 +30,7 @@ public function getServer($key) { return $this->server[$key]; }
public function getSession($key) { return $this->session[$key]; }
public function getGet($key) { return $this->get[$key]; }
public function getPost($key) { return $this->post[$key]; }
+ public function getScript() { return $this->script; }
public function setServer($key, $value) { $this->server[$key] = $value; }
public function setSession($key, $value) { $this->session[$key] = $value; }
@@ -70,13 +74,13 @@ public function execute($uri='/doku.php') {
// now execute dokuwiki and grep the output
header_remove();
ob_start('ob_start_callback');
- include(DOKU_INC.'doku.php');
+ include(DOKU_INC.$this->script);
ob_end_flush();
// create the response object
$response = new TestResponse(
$output_buffer,
- headers_list()
+ (function_exists('xdebug_get_headers') ? xdebug_get_headers() : headers_list()) // cli sapi doesn't do headers, prefer xdebug_get_headers() which works under cli
);
// reset environment
@@ -102,14 +106,15 @@ public function execute($uri='/doku.php') {
* @todo make this work with other end points
*/
protected function setUri($uri){
- if(substr($uri,0,9) != '/doku.php'){
- throw new Exception("only '/doku.php' is supported currently");
+ if(!preg_match('#^('.join('|',$this->valid_scripts).')#',$uri)){
+ throw new Exception("$uri \n--- only ".join(', ',$this->valid_scripts)." are supported currently");
}
$params = array();
list($uri, $query) = explode('?',$uri,2);
if($query) parse_str($query, $params);
+ $this->script = substr($uri,1);
$this->get = array_merge($params, $this->get);
if(count($this->get)){
$query = '?'.http_build_query($this->get, '', '&');
@@ -129,7 +134,7 @@ protected function setUri($uri){
* Simulate a POST request with the given variables
*
* @param array $post all the POST parameters to use
- * @param string $url end URL to simulate, needs to start with /doku.php currently
+ * @param string $url end URL to simulate, needs to start with /doku.php, /lib/exe/fetch.php or /lib/exe/detail.php currently
* @param return TestResponse
*/
public function post($post=array(), $uri='/doku.php') {
@@ -141,8 +146,8 @@ public function post($post=array(), $uri='/doku.php') {
/**
* Simulate a GET request with the given variables
*
- * @param array $GET all the POST parameters to use
- * @param string $url end URL to simulate, needs to start with /doku.php currently
+ * @param array $GET all the GET parameters to use
+ * @param string $url end URL to simulate, needs to start with /doku.php, /lib/exe/fetch.php or /lib/exe/detail.php currently
* @param return TestResponse
*/
public function get($get=array(), $uri='/doku.php') {
View
38 _test/core/TestResponse.php
@@ -42,6 +42,44 @@ public function getHeaders() {
}
/**
+ * @param $name string, the name of the header without the ':', e.g. 'Content-Type', 'Pragma'
+ * @return mixed if exactly one header, the header (string); otherwise an array of headers, empty when no headers
+ */
+ public function getHeader($name) {
+ $result = array();
+ foreach ($this->headers as $header) {
+ if (substr($header,0,strlen($name)+1) == $name.':') {
+ $result[] = $header;
+ }
+ }
+
+ return count($result) == 1 ? $result[0] : $result;
+ }
+
+ /**
+ * @return int http status code
+ *
+ * in the test environment, only status codes explicitly set by dokuwiki are likely to be returned
+ * this means succcessful status codes (e.g. 200 OK) will not be present, but error codes will be
+ */
+ public function getStatusCode() {
+ $headers = $this->getHeader('Status');
+ $code = null;
+
+ if ($headers) {
+ // if there is more than one status header, use the last one
+ $status = is_array($headers) ? array_pop($headers) : $headers;
+ $matches = array();
+ preg_match('/^Status: ?(\d+)/',$status,$matches);
+ if ($matches){
+ $code = $matches[1];
+ }
+ }
+
+ return $code;
+ }
+
+ /**
* Query the response for a JQuery compatible CSS selector
*
* @link https://code.google.com/p/phpquery/wiki/Selectors
View
99 _test/tests/lib/exe/fetch_imagetoken.test.php
@@ -0,0 +1,99 @@
+<?php
+
+class fetch_imagetoken_test extends DokuWikiTest {
+
+ private $media = 'wiki:dokuwiki-128.png';
+ private $width = 200;
+ private $height = 0;
+
+ function setUp() {
+ // check we can carry out these tests
+ if (!file_exists(mediaFN($this->media))) {
+ $this->markTestSkipped('Source image required for test');
+ }
+
+ header('X-Test: check headers working');
+ $header_check = function_exists('xdebug_get_headers') ? xdebug_get_headers() : headers_list();
+ if (empty($header_check)) {
+ $this->markTestSkipped('headers not returned, perhaps your sapi does not return headers, try xdebug');
+ } else {
+ header_remove('X-Test');
+ }
+
+ parent::setUp();
+
+ global $conf;
+ $conf['sendfile'] = 0;
+
+ global $MIME, $EXT, $CACHE, $INPUT; // variables fetch creates in global scope -- should this be in fetch?
+ }
+
+ function getUri() {
+ $w = $this->width ? 'w='.$this->width.'&' : '';
+ $h = $this->height ? 'h='.$this->height.'&' : '';
+
+ return '/lib/exe/fetch.php?'.$w.$h.'{%token%}media='.$this->media;
+ }
+
+ function fetchResponse($token){
+ $request = new TestRequest();
+ return $request->get(array(),str_replace('{%token%}',$token,$this->getUri()));
+ }
+
+ /**
+ * modified image request with valid token
+ * expect: header with mime-type
+ * expect: content
+ * expect: no error response
+ */
+ function test_valid_token(){
+ $valid_token = 'tok='.media_get_token($this->media, $this->width, $this->height).'&';
+ $response = $this->fetchResponse($valid_token);
+ $this->assertTrue((bool)$response->getHeader('Content-Type'));
+ $this->assertTrue((bool)($response->getContent()));
+
+ $status_code = $response->getStatusCode();
+ $this->assertTrue(is_null($status_code) || (200 == $status_code));
+ }
+
+ /**
+ * modified image request with invalid token
+ * expect: 412 status code
+ */
+ function test_invalid_token(){
+ $invalid_token = 'tok='.media_get_token('junk',200,100).'&';
+ $this->assertEquals(412,$this->fetchResponse($invalid_token)->getStatusCode());
+ }
+
+ /**
+ * modified image request with no token
+ * expect: 412 status code
+ */
+ function test_missing_token(){
+ $no_token = '';
+ $this->assertEquals(412,$this->fetchResponse($notoken)->getStatusCode());
+ }
+
+ /**
+ * native image request which doesn't require a token
+ * try: with a token & without a token
+ * expect: (for both) header with mime-type, content matching source image filesize & no error response
+ */
+ function test_no_token_required(){
+ $this->width = $this->height = 0; // no width & height, means image request at native dimensions
+ $any_token = 'tok='.media_get_token('junk',200,100).'&';
+ $no_token = '';
+ $bytes = filesize(mediaFN($this->media));
+
+ foreach(array($any_token, $no_token) as $token) {
+ $response = $this->fetchResponse($token);
+ $this->assertTrue((bool)$response->getHeader('Content-Type'));
+ $this->assertEquals(strlen($response->getContent()), $bytes);
+
+ $status_code = $response->getStatusCode();
+ $this->assertTrue(is_null($status_code) || (200 == $status_code));
+ }
+ }
+
+}
+//Setup VIM: ex: et ts=4 :
View
67 _test/tests/test/basic.test.php
@@ -4,6 +4,24 @@
* @group integration
*/
class InttestsBasicTest extends DokuWikiTest {
+
+ private $some_headers = array(
+ 'Content-Type: image/png',
+ 'Date: Fri, 22 Mar 2013 16:10:01 GMT',
+ 'X-Powered-By: PHP/5.3.15',
+ 'Expires: Sat, 23 Mar 2013 17:03:46 GMT',
+ 'Cache-Control: public, proxy-revalidate, no-transform, max-age=86400',
+ 'Pragma: public',
+ 'Last-Modified: Fri, 22 Mar 2013 01:48:28 GMT',
+ 'ETag: "63daab733b38c30c337229b2e587f8fb"',
+ 'Content-Disposition: inline; filename="fe389b0db8c1088c336abb502d2f9ae7.media.200x200.png',
+ 'Accept-Ranges: bytes',
+ 'Content-Type: image/png',
+ 'Content-Length: 62315',
+ 'Status: 200 OK',
+ 'Status: 404 Not Found',
+ );
+
/**
* Execute the simplest possible request and expect
* a dokuwiki page which obviously has the word "DokuWiki"
@@ -101,5 +119,54 @@ function testGet() {
$this->assertTrue(strpos($response->getContent(), 'Andreas Gohr') >= 0);
}
+ function testScripts() {
+ $request = new TestRequest();
+
+ // doku
+ $response = $request->get();
+ $this->assertEquals('doku.php',$request->getScript());
+
+ $response = $request->get(array(),'/doku.php?id=wiki:dokuwiki&test=foo');
+ $this->assertEquals('doku.php',$request->getScript());
+
+ // fetch
+ $response = $request->get(array(),'/lib/exe/fetch.php?media=wiki:dokuwiki-128.png');
+ $this->assertEquals('lib/exe/fetch.php',$request->getScript());
+
+ // detail
+ $response = $request->get(array(),'/lib/exe/detail.php?id=start&media=wiki:dokuwiki-128.png');
+ $this->assertEquals('lib/exe/detail.php',$request->getScript());
+ }
+
+ function testHeaders(){
+ header('X-Test: check headers working');
+ $header_check = function_exists('xdebug_get_headers') ? xdebug_get_headers() : headers_list();
+ if (empty($header_check)) {
+ $this->markTestSkipped('headers not returned, perhaps your sapi does not return headers, try xdebug');
+ } else {
+ header_remove('X-Test');
+ }
+
+ $request = new TestRequest();
+ $response = $request->get(array(),'/lib/exe/fetch.php?media=wiki:dokuwiki-128.png');
+ $headers = $response->getHeaders();
+ $this->assertTrue(!empty($headers));
+ }
+
+ function testGetHeader(){
+ $response = new TestResponse('',$this->some_headers);
+
+ $this->assertEquals('Pragma: public', $response->getHeader('Pragma'));
+ $this->assertEmpty($response->getHeader('Junk'));
+ $this->assertEquals(array('Content-Type: image/png','Content-Type: image/png'), $response->getHeader('Content-Type'));
+ }
+
+ function testGetStatus(){
+ $response = new TestResponse('',$this->some_headers);
+ $this->assertEquals(404, $response->getStatusCode());
+
+ $response = new TestResponse('',array_slice($this->some_headers,0,-2)); // slice off the last two headers to leave no status header
+ $this->assertNull($response->getStatusCode());
+ }
}
View
4 inc/common.php
@@ -436,6 +436,10 @@ function exportlink($id = '', $format = 'raw', $more = '', $abs = false, $sep =
function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false) {
global $conf;
if(is_array($more)) {
+ // add token for resized images
+ if($more['w'] || $more['h']){
+ $more['tok'] = media_get_token($id,$more['w'],$more['h']);
+ }
// strip defaults for shorter URLs
if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
if(!$more['w']) unset($more['w']);
View
149 inc/fetch.functions.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Functions used by lib/exe/fetch.php
+ * (not included by other parts of dokuwiki)
+ */
+
+/**
+ * Set headers and send the file to the client
+ *
+ * The $cache parameter influences how long files may be kept in caches, the $public parameter
+ * influences if this caching may happen in public proxis or in the browser cache only FS#2734
+ *
+ * This function will abort the current script when a 304 is sent or file sending is handled
+ * through x-sendfile
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @param string $file local file to send
+ * @param string $mime mime type of the file
+ * @param bool $dl set to true to force a browser download
+ * @param int $cache remaining cache time in seconds (-1 for $conf['cache'], 0 for no-cache)
+ * @param bool $public is this a public ressource or a private one?
+ */
+function sendFile($file, $mime, $dl, $cache, $public = false) {
+ global $conf;
+ // send mime headers
+ header("Content-Type: $mime");
+
+ // calculate cache times
+ if($cache == -1) {
+ $maxage = max($conf['cachetime'], 3600); // cachetime or one hour
+ $expires = time() + $maxage;
+ } else if($cache > 0) {
+ $maxage = $cache; // given time
+ $expires = time() + $maxage;
+ } else { // $cache == 0
+ $maxage = 0;
+ $expires = 0; // 1970-01-01
+ }
+
+ // smart http caching headers
+ if($maxage) {
+ if($public) {
+ // cache publically
+ header('Expires: '.gmdate("D, d M Y H:i:s", $expires).' GMT');
+ header('Cache-Control: public, proxy-revalidate, no-transform, max-age='.$maxage);
+ header('Pragma: public');
+ } else {
+ // cache in browser
+ header('Expires: '.gmdate("D, d M Y H:i:s", $expires).' GMT');
+ header('Cache-Control: private, no-transform, max-age='.$maxage);
+ header('Pragma: no-cache');
+ }
+ } else {
+ // no cache at all
+ header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
+ header('Cache-Control: no-cache, no-transform');
+ header('Pragma: no-cache');
+ }
+
+ //send important headers first, script stops here if '304 Not Modified' response
+ $fmtime = @filemtime($file);
+ http_conditionalRequest($fmtime);
+
+ //download or display?
+ if($dl) {
+ header('Content-Disposition: attachment; filename="'.utf8_basename($file).'";');
+ } else {
+ header('Content-Disposition: inline; filename="'.utf8_basename($file).'";');
+ }
+
+ //use x-sendfile header to pass the delivery to compatible webservers
+ if(http_sendfile($file)) exit;
+
+ // send file contents
+ $fp = @fopen($file, "rb");
+ if($fp) {
+ http_rangeRequest($fp, filesize($file), $mime);
+ } else {
+ http_status(500);
+ print "Could not read $file - bad permissions?";
+ }
+}
+
+/**
+ * Check for media for preconditions and return correct status code
+ *
+ * READ: MEDIA, MIME, EXT, CACHE
+ * WRITE: MEDIA, FILE, array( STATUS, STATUSMESSAGE )
+ *
+ * @author Gerry Weissbach <gerry.w@gammaproduction.de>
+ * @param $media reference to the media id
+ * @param $file reference to the file variable
+ * @returns array(STATUS, STATUSMESSAGE)
+ */
+function checkFileStatus(&$media, &$file, $rev = '', $width=0, $height=0) {
+ global $MIME, $EXT, $CACHE, $INPUT;
+
+ //media to local file
+ if(preg_match('#^(https?)://#i', $media)) {
+ //check hash
+ if(substr(md5(auth_cookiesalt().$media), 0, 6) !== $INPUT->str('hash')) {
+ return array(412, 'Precondition Failed');
+ }
+ //handle external images
+ if(strncmp($MIME, 'image/', 6) == 0) $file = media_get_from_URL($media, $EXT, $CACHE);
+ if(!$file) {
+ //download failed - redirect to original URL
+ return array(302, $media);
+ }
+ } else {
+ $media = cleanID($media);
+ if(empty($media)) {
+ return array(400, 'Bad request');
+ }
+ // check token for resized images
+ if (($width || $height) && media_get_token($media, $width, $height) !== $INPUT->str('tok')) {
+ return array(412, 'Precondition Failed');
+ }
+
+ //check permissions (namespace only)
+ if(auth_quickaclcheck(getNS($media).':X') < AUTH_READ) {
+ return array(403, 'Forbidden');
+ }
+ $file = mediaFN($media, $rev);
+ }
+
+ //check file existance
+ if(!@file_exists($file)) {
+ return array(404, 'Not Found');
+ }
+
+ return array(200, null);
+}
+
+/**
+ * Returns the wanted cachetime in seconds
+ *
+ * Resolves named constants
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function calc_cache($cache) {
+ global $conf;
+
+ if(strtolower($cache) == 'nocache') return 0; //never cache
+ if(strtolower($cache) == 'recache') return $conf['cachetime']; //use standard cache
+ return -1; //cache endless
+}
View
9 inc/httputils.php
@@ -61,9 +61,9 @@ function http_conditionalRequest($timestamp){
}
/**
- * Let the webserver send the given file vi x-sendfile method
+ * Let the webserver send the given file via x-sendfile method
*
- * @author Chris Smith <chris.eureka@jalakai.co.uk>
+ * @author Chris Smith <chris@jalakai.co.uk>
* @returns void or exits with previously header() commands executed
*/
function http_sendfile($file) {
@@ -177,7 +177,8 @@ function http_rangeRequest($fh,$size,$mime){
echo HTTP_HEADER_LF.'--'.HTTP_MULTIPART_BOUNDARY.'--'.HTTP_HEADER_LF;
}
- // everything should be done here, exit
+ // everything should be done here, exit (or return if testing)
+ if (defined('SIMPLE_TEST')) return;
exit;
}
@@ -320,7 +321,7 @@ function http_status($code = 200, $text = '') {
$server_protocol = (isset($_SERVER['SERVER_PROTOCOL'])) ? $_SERVER['SERVER_PROTOCOL'] : false;
- if(substr(php_sapi_name(), 0, 3) == 'cgi') {
+ if(substr(php_sapi_name(), 0, 3) == 'cgi' || defined('SIMPLE_TEST')) {
header("Status: {$code} {$text}", true);
} elseif($server_protocol == 'HTTP/1.1' OR $server_protocol == 'HTTP/1.0') {
header($server_protocol." {$code} {$text}", true, $code);
View
24 inc/media.php
@@ -1865,6 +1865,30 @@ function media_crop_image($file, $ext, $w, $h=0){
}
/**
+ * Calculate a token to be used to verify fetch requests for resized or
+ * cropped images have been internally generated - and prevent external
+ * DDOS attacks via fetch
+ *
+ * @param string $id id of the image
+ * @param int $w resize/crop width
+ * @param int $h resize/crop height
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+function media_get_token($id,$w,$h){
+ // token is only required for modified images
+ if ($w || $h) {
+ $token = auth_cookiesalt().$id;
+ if ($w) $token .= '.'.$w;
+ if ($h) $token .= '.'.$h;
+
+ return substr(md5($token),0,6);
+ }
+
+ return '';
+}
+
+/**
* Download a remote file and return local filename
*
* returns false if download fails. Uses cached file if available and
View
158 lib/exe/fetch.php
@@ -7,12 +7,17 @@
*/
if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__).'/../../');
-define('DOKU_DISABLE_GZIP_OUTPUT', 1);
+if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1);
require_once(DOKU_INC.'inc/init.php');
session_write_close(); //close session
-// BEGIN main (if not testing)
-if(!defined('SIMPLE_TEST')) {
+require_once(DOKU_INC.'inc/fetch.functions.php');
+
+if (defined('SIMPLE_TEST')) {
+ $INPUT = new Input();
+}
+
+// BEGIN main
$mimetypes = getMimeTypes();
//get input
@@ -32,7 +37,7 @@
}
// check for permissions, preconditions and cache external files
- list($STATUS, $STATUSMESSAGE) = checkFileStatus($MEDIA, $FILE, $REV);
+ list($STATUS, $STATUSMESSAGE) = checkFileStatus($MEDIA, $FILE, $REV, $WIDTH, $HEIGHT);
// prepare data for plugin events
$data = array(
@@ -64,6 +69,7 @@
// die on errors
if($data['status'] > 203) {
print $data['statusmessage'];
+ if (defined('SIMPLE_TEST')) return;
exit;
}
}
@@ -87,148 +93,6 @@
// Do something after the download finished.
$evt->advise_after(); // will not be emitted on 304 or x-sendfile
-}// END DO main
-
-/* ------------------------------------------------------------------------ */
-
-/**
- * Set headers and send the file to the client
- *
- * The $cache parameter influences how long files may be kept in caches, the $public parameter
- * influences if this caching may happen in public proxis or in the browser cache only FS#2734
- *
- * This function will abort the current script when a 304 is sent or file sending is handled
- * through x-sendfile
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- * @author Ben Coburn <btcoburn@silicodon.net>
- * @param string $file local file to send
- * @param string $mime mime type of the file
- * @param bool $dl set to true to force a browser download
- * @param int $cache remaining cache time in seconds (-1 for $conf['cache'], 0 for no-cache)
- * @param bool $public is this a public ressource or a private one?
- */
-function sendFile($file, $mime, $dl, $cache, $public = false) {
- global $conf;
- // send mime headers
- header("Content-Type: $mime");
-
- // calculate cache times
- if($cache == -1) {
- $maxage = max($conf['cachetime'], 3600); // cachetime or one hour
- $expires = time() + $maxage;
- } else if($cache > 0) {
- $maxage = $cache; // given time
- $expires = time() + $maxage;
- } else { // $cache == 0
- $maxage = 0;
- $expires = 0; // 1970-01-01
- }
-
- // smart http caching headers
- if($maxage) {
- if($public) {
- // cache publically
- header('Expires: '.gmdate("D, d M Y H:i:s", $expires).' GMT');
- header('Cache-Control: public, proxy-revalidate, no-transform, max-age='.$maxage);
- header('Pragma: public');
- } else {
- // cache in browser
- header('Expires: '.gmdate("D, d M Y H:i:s", $expires).' GMT');
- header('Cache-Control: private, no-transform, max-age='.$maxage);
- header('Pragma: no-cache');
- }
- } else {
- // no cache at all
- header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
- header('Cache-Control: no-cache, no-transform');
- header('Pragma: no-cache');
- }
-
- //send important headers first, script stops here if '304 Not Modified' response
- $fmtime = @filemtime($file);
- http_conditionalRequest($fmtime);
-
- //download or display?
- if($dl) {
- header('Content-Disposition: attachment; filename="'.utf8_basename($file).'";');
- } else {
- header('Content-Disposition: inline; filename="'.utf8_basename($file).'";');
- }
-
- //use x-sendfile header to pass the delivery to compatible webservers
- if(http_sendfile($file)) exit;
-
- // send file contents
- $fp = @fopen($file, "rb");
- if($fp) {
- http_rangeRequest($fp, filesize($file), $mime);
- } else {
- http_status(500);
- print "Could not read $file - bad permissions?";
- }
-}
-
-/**
- * Check for media for preconditions and return correct status code
- *
- * READ: MEDIA, MIME, EXT, CACHE
- * WRITE: MEDIA, FILE, array( STATUS, STATUSMESSAGE )
- *
- * @author Gerry Weissbach <gerry.w@gammaproduction.de>
- * @param $media reference to the media id
- * @param $file reference to the file variable
- * @returns array(STATUS, STATUSMESSAGE)
- */
-function checkFileStatus(&$media, &$file, $rev = '') {
- global $MIME, $EXT, $CACHE, $INPUT;
-
- //media to local file
- if(preg_match('#^(https?)://#i', $media)) {
- //check hash
- if(substr(md5(auth_cookiesalt().$media), 0, 6) !== $INPUT->str('hash')) {
- return array(412, 'Precondition Failed');
- }
- //handle external images
- if(strncmp($MIME, 'image/', 6) == 0) $file = media_get_from_URL($media, $EXT, $CACHE);
- if(!$file) {
- //download failed - redirect to original URL
- return array(302, $media);
- }
- } else {
- $media = cleanID($media);
- if(empty($media)) {
- return array(400, 'Bad request');
- }
-
- //check permissions (namespace only)
- if(auth_quickaclcheck(getNS($media).':X') < AUTH_READ) {
- return array(403, 'Forbidden');
- }
- $file = mediaFN($media, $rev);
- }
-
- //check file existance
- if(!@file_exists($file)) {
- return array(404, 'Not Found');
- }
-
- return array(200, null);
-}
-
-/**
- * Returns the wanted cachetime in seconds
- *
- * Resolves named constants
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- */
-function calc_cache($cache) {
- global $conf;
-
- if(strtolower($cache) == 'nocache') return 0; //never cache
- if(strtolower($cache) == 'recache') return $conf['cachetime']; //use standard cache
- return -1; //cache endless
-}
+// END DO main
//Setup VIM: ex: et ts=2 :
Something went wrong with that request. Please try again.