SECURITY: API: Improve validation in chunked uploading
This fixes a few shortcomings in the chunked uploader: * Raises an error if offset + chunksize > filesize. * Enforces a minimum chunk size for non-final chunks. * Refuses additional chunks after seeing a final chunk. * Status of a chunked upload in progress is now available with 'checkstatus'. Bug: T91203 Bug: T91205 Change-Id: I2262db1bc8460616b069c564475d2e4148001768
This commit is contained in:
parent
c804391572
commit
59b627b0b7
5 changed files with 90 additions and 16 deletions
|
|
@ -720,6 +720,14 @@ $wgCopyUploadAsyncTimeout = false;
|
||||||
*/
|
*/
|
||||||
$wgMaxUploadSize = 1024 * 1024 * 100; # 100MB
|
$wgMaxUploadSize = 1024 * 1024 * 100; # 100MB
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum upload chunk size, in bytes. When using chunked upload, non-final
|
||||||
|
* chunks smaller than this will be rejected. May be reduced based on the
|
||||||
|
* 'upload_max_filesize' or 'post_max_size' PHP settings.
|
||||||
|
* @since 1.26
|
||||||
|
*/
|
||||||
|
$wgMinUploadChunkSize = 1024; # 1KB
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Point the upload navigation link to an external URL
|
* Point the upload navigation link to an external URL
|
||||||
* Useful if you want to use a shared repository by default
|
* Useful if you want to use a shared repository by default
|
||||||
|
|
|
||||||
|
|
@ -3940,12 +3940,13 @@ function wfTransactionalTimeLimit() {
|
||||||
* Converts shorthand byte notation to integer form
|
* Converts shorthand byte notation to integer form
|
||||||
*
|
*
|
||||||
* @param string $string
|
* @param string $string
|
||||||
|
* @param int $default Returned if $string is empty
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
function wfShorthandToInteger( $string = '' ) {
|
function wfShorthandToInteger( $string = '', $default = -1 ) {
|
||||||
$string = trim( $string );
|
$string = trim( $string );
|
||||||
if ( $string === '' ) {
|
if ( $string === '' ) {
|
||||||
return -1;
|
return $default;
|
||||||
}
|
}
|
||||||
$last = $string[strlen( $string ) - 1];
|
$last = $string[strlen( $string ) - 1];
|
||||||
$val = intval( $string );
|
$val = intval( $string );
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,15 @@ if ( $wgResourceLoaderMaxQueryLength === false ) {
|
||||||
unset( $suhosinMaxValueLength );
|
unset( $suhosinMaxValueLength );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the minimum chunk size is less than PHP upload limits or the maximum
|
||||||
|
// upload size.
|
||||||
|
$wgMinUploadChunkSize = min(
|
||||||
|
$wgMinUploadChunkSize,
|
||||||
|
$wgMaxUploadSize,
|
||||||
|
wfShorthandToInteger( ini_get( 'upload_max_filesize' ), 1e100 ),
|
||||||
|
wfShorthandToInteger( ini_get( 'post_max_size' ), 1e100 ) - 1024 # Leave room for other parameters
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definitions of the NS_ constants are in Defines.php
|
* Definitions of the NS_ constants are in Defines.php
|
||||||
* @private
|
* @private
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
|
||||||
$data['misermode'] = (bool)$config->get( 'MiserMode' );
|
$data['misermode'] = (bool)$config->get( 'MiserMode' );
|
||||||
|
|
||||||
$data['maxuploadsize'] = UploadBase::getMaxUploadSize();
|
$data['maxuploadsize'] = UploadBase::getMaxUploadSize();
|
||||||
|
$data['minuploadchunksize'] = (int)$this->getConfig()->get( 'MinUploadChunkSize' );
|
||||||
|
|
||||||
$data['thumblimits'] = $config->get( 'ThumbLimits' );
|
$data['thumblimits'] = $config->get( 'ThumbLimits' );
|
||||||
ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' );
|
ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' );
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class ApiUpload extends ApiBase {
|
||||||
|
|
||||||
// Check if the uploaded file is sane
|
// Check if the uploaded file is sane
|
||||||
if ( $this->mParams['chunk'] ) {
|
if ( $this->mParams['chunk'] ) {
|
||||||
$maxSize = $this->mUpload->getMaxUploadSize();
|
$maxSize = UploadBase::getMaxUploadSize();
|
||||||
if ( $this->mParams['filesize'] > $maxSize ) {
|
if ( $this->mParams['filesize'] > $maxSize ) {
|
||||||
$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
|
$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
|
||||||
}
|
}
|
||||||
|
|
@ -204,13 +204,30 @@ class ApiUpload extends ApiBase {
|
||||||
private function getChunkResult( $warnings ) {
|
private function getChunkResult( $warnings ) {
|
||||||
$result = array();
|
$result = array();
|
||||||
|
|
||||||
$result['result'] = 'Continue';
|
|
||||||
if ( $warnings && count( $warnings ) > 0 ) {
|
if ( $warnings && count( $warnings ) > 0 ) {
|
||||||
$result['warnings'] = $warnings;
|
$result['warnings'] = $warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
$request = $this->getMain()->getRequest();
|
$request = $this->getMain()->getRequest();
|
||||||
$chunkPath = $request->getFileTempname( 'chunk' );
|
$chunkPath = $request->getFileTempname( 'chunk' );
|
||||||
$chunkSize = $request->getUpload( 'chunk' )->getSize();
|
$chunkSize = $request->getUpload( 'chunk' )->getSize();
|
||||||
|
$totalSoFar = $this->mParams['offset'] + $chunkSize;
|
||||||
|
$minChunkSize = $this->getConfig()->get( 'MinUploadChunkSize' );
|
||||||
|
|
||||||
|
// Sanity check sizing
|
||||||
|
if ( $totalSoFar > $this->mParams['filesize'] ) {
|
||||||
|
$this->dieUsage(
|
||||||
|
'Offset plus current chunk is greater than claimed file size', 'invalid-chunk'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce minimum chunk size
|
||||||
|
if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
|
||||||
|
$this->dieUsage(
|
||||||
|
"Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( $this->mParams['offset'] == 0 ) {
|
if ( $this->mParams['offset'] == 0 ) {
|
||||||
try {
|
try {
|
||||||
$filekey = $this->performStash();
|
$filekey = $this->performStash();
|
||||||
|
|
@ -222,6 +239,18 @@ class ApiUpload extends ApiBase {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$filekey = $this->mParams['filekey'];
|
$filekey = $this->mParams['filekey'];
|
||||||
|
|
||||||
|
// Don't allow further uploads to an already-completed session
|
||||||
|
$progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
|
||||||
|
if ( !$progress ) {
|
||||||
|
// Probably can't get here, but check anyway just in case
|
||||||
|
$this->dieUsage( 'No chunked upload session with this key', 'stashfailed' );
|
||||||
|
} elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
|
||||||
|
$this->dieUsage(
|
||||||
|
'Chunked upload is already completed, check status for details', 'stashfailed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$status = $this->mUpload->addChunk(
|
$status = $this->mUpload->addChunk(
|
||||||
$chunkPath, $chunkSize, $this->mParams['offset'] );
|
$chunkPath, $chunkSize, $this->mParams['offset'] );
|
||||||
if ( !$status->isGood() ) {
|
if ( !$status->isGood() ) {
|
||||||
|
|
@ -230,18 +259,12 @@ class ApiUpload extends ApiBase {
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->dieUsage( $status->getWikiText(), 'stashfailed', 0, $extradata );
|
$this->dieUsage( $status->getWikiText(), 'stashfailed', 0, $extradata );
|
||||||
|
|
||||||
return array();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check we added the last chunk:
|
// Check we added the last chunk:
|
||||||
if ( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) {
|
if ( $totalSoFar == $this->mParams['filesize'] ) {
|
||||||
if ( $this->mParams['async'] ) {
|
if ( $this->mParams['async'] ) {
|
||||||
$progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
|
|
||||||
if ( $progress && $progress['result'] === 'Poll' ) {
|
|
||||||
$this->dieUsage( "Chunk assembly already in progress.", 'stashfailed' );
|
|
||||||
}
|
|
||||||
UploadBase::setSessionStatus(
|
UploadBase::setSessionStatus(
|
||||||
$this->getUser(),
|
$this->getUser(),
|
||||||
$filekey,
|
$filekey,
|
||||||
|
|
@ -261,21 +284,38 @@ class ApiUpload extends ApiBase {
|
||||||
} else {
|
} else {
|
||||||
$status = $this->mUpload->concatenateChunks();
|
$status = $this->mUpload->concatenateChunks();
|
||||||
if ( !$status->isGood() ) {
|
if ( !$status->isGood() ) {
|
||||||
|
UploadBase::setSessionStatus(
|
||||||
|
$this->getUser(),
|
||||||
|
$filekey,
|
||||||
|
array( 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status )
|
||||||
|
);
|
||||||
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
|
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
|
||||||
|
|
||||||
return array();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fully concatenated file has a new filekey. So remove
|
// The fully concatenated file has a new filekey. So remove
|
||||||
// the old filekey and fetch the new one.
|
// the old filekey and fetch the new one.
|
||||||
|
UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
|
||||||
$this->mUpload->stash->removeFile( $filekey );
|
$this->mUpload->stash->removeFile( $filekey );
|
||||||
$filekey = $this->mUpload->getLocalFile()->getFileKey();
|
$filekey = $this->mUpload->getLocalFile()->getFileKey();
|
||||||
|
|
||||||
$result['result'] = 'Success';
|
$result['result'] = 'Success';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
UploadBase::setSessionStatus(
|
||||||
|
$this->getUser(),
|
||||||
|
$filekey,
|
||||||
|
array(
|
||||||
|
'result' => 'Continue',
|
||||||
|
'stage' => 'uploading',
|
||||||
|
'offset' => $totalSoFar,
|
||||||
|
'status' => Status::newGood(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$result['result'] = 'Continue';
|
||||||
|
$result['offset'] = $totalSoFar;
|
||||||
}
|
}
|
||||||
|
|
||||||
$result['filekey'] = $filekey;
|
$result['filekey'] = $filekey;
|
||||||
$result['offset'] = $this->mParams['offset'] + $chunkSize;
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
@ -385,6 +425,10 @@ class ApiUpload extends ApiBase {
|
||||||
// Chunk upload
|
// Chunk upload
|
||||||
$this->mUpload = new UploadFromChunks();
|
$this->mUpload = new UploadFromChunks();
|
||||||
if ( isset( $this->mParams['filekey'] ) ) {
|
if ( isset( $this->mParams['filekey'] ) ) {
|
||||||
|
if ( $this->mParams['offset'] === 0 ) {
|
||||||
|
$this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
|
||||||
|
}
|
||||||
|
|
||||||
// handle new chunk
|
// handle new chunk
|
||||||
$this->mUpload->continueChunks(
|
$this->mUpload->continueChunks(
|
||||||
$this->mParams['filename'],
|
$this->mParams['filename'],
|
||||||
|
|
@ -392,6 +436,10 @@ class ApiUpload extends ApiBase {
|
||||||
$request->getUpload( 'chunk' )
|
$request->getUpload( 'chunk' )
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
if ( $this->mParams['offset'] !== 0 ) {
|
||||||
|
$this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' );
|
||||||
|
}
|
||||||
|
|
||||||
// handle first chunk
|
// handle first chunk
|
||||||
$this->mUpload->initialize(
|
$this->mUpload->initialize(
|
||||||
$this->mParams['filename'],
|
$this->mParams['filename'],
|
||||||
|
|
@ -793,8 +841,15 @@ class ApiUpload extends ApiBase {
|
||||||
),
|
),
|
||||||
'stash' => false,
|
'stash' => false,
|
||||||
|
|
||||||
'filesize' => null,
|
'filesize' => array(
|
||||||
'offset' => null,
|
ApiBase::PARAM_TYPE => 'integer',
|
||||||
|
ApiBase::PARAM_MIN => 0,
|
||||||
|
ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
|
||||||
|
),
|
||||||
|
'offset' => array(
|
||||||
|
ApiBase::PARAM_TYPE => 'integer',
|
||||||
|
ApiBase::PARAM_MIN => 0,
|
||||||
|
),
|
||||||
'chunk' => array(
|
'chunk' => array(
|
||||||
ApiBase::PARAM_TYPE => 'upload',
|
ApiBase::PARAM_TYPE => 'upload',
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue