Skip to content

fseek does not work with php://input when data is not pre-read #9441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
bohwaz opened this issue Aug 28, 2022 · 15 comments
Closed

fseek does not work with php://input when data is not pre-read #9441

bohwaz opened this issue Aug 28, 2022 · 15 comments

Comments

@bohwaz
Copy link
Contributor

bohwaz commented Aug 28, 2022

Description

$a = fopen('php://input', 'r');

var_dump(stream_get_meta_data($a)['seekable']);

var_dump(ftell($a));
var_dump(fseek($a, 10, SEEK_SET));
var_dump(ftell($a));

Resulted in this output:

bool(true)
int(0)
int(-1)
bool(false)

But I expected this output instead:

bool(true)
int(0)
int(0)
int(10)

If you actually read through to the end of the stream, it becomes seekable:

$a = fopen('php://input', 'r');

while (!feof($a)) {
	fread($a, 8192);
}

var_dump(stream_get_meta_data($a)['seekable']);

var_dump(ftell($a));
var_dump(fseek($a, 10, SEEK_SET));
var_dump(ftell($a));
var_dump(fseek($a, 0, SEEK_END));
var_dump(ftell($a));

Results in what is expected:

bool(true)
int(30320)
int(0)
int(10)
int(0)
int(30320)

PHP Version

PHP/8.1.9

Operating System

Debian stable

@bohwaz bohwaz changed the title fseek does not work with php://input even though the stream is advertised as streamable fseek does not work with php://input even though the stream is advertised as seekable Aug 28, 2022
@cmb69
Copy link
Member

cmb69 commented Aug 29, 2022

Which SAPI are you using? cli?

@bohwaz
Copy link
Contributor Author

bohwaz commented Aug 29, 2022

@cmb69 I get the same results with Apache and mod_php (Apache 2.4.54) and PHP CLI server.

@cmb69
Copy link
Member

cmb69 commented Aug 29, 2022

I cannot reproduce this (neither with Apache mod_php nor with the builtin server), assuming the request body is not empty; maybe Windows is not affected.

@bohwaz
Copy link
Contributor Author

bohwaz commented Aug 29, 2022

Oh yeah I'm on debian. I don't have a Windows to test stuff.

Also what made me fall into this was the fact that fseek($a, 0, SEEK_END) will return 0 instead of -1, and then ftell would be at 0 like if the file was empty.

Here is a complete curl trace using the first code example:

% curl -v http://localhost:8080/ --upload-file ~/images/wallpaper.jpg 
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> PUT /wallpaper.jpg HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Length: 743615
> Expect: 100-continue
> 
* Done waiting for 100-continue
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8080
< Date: Mon, 29 Aug 2022 15:23:28 GMT
< Connection: close
< X-Powered-By: PHP/8.1.9
< Content-type: text/html; charset=UTF-8
< 
bool(true)
int(0)
int(-1)
bool(false)
* Closing connection 0

@bohwaz
Copy link
Contributor Author

bohwaz commented Aug 29, 2022

Also before anyone asks, this is not related to curl using 100 Continue HTTP feature:

 % curl -v http://localhost:8080/ --upload-file ~/images/wallpaper.jpg -H 'Expect:'
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> PUT /wallpaper.jpg HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Length: 743615
> 
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8080
< Date: Mon, 29 Aug 2022 15:28:00 GMT
< Connection: close
< X-Powered-By: PHP/8.1.9
< Content-type: text/html; charset=UTF-8
< 
bool(true)
int(0)
int(-1)
bool(false)
* Closing connection 0

@cmb69
Copy link
Member

cmb69 commented Aug 30, 2022

PUT /wallpaper.jpg HTTP/1.1

Ah, you've made a PUT request; I've tried with a POST. Not sure if there's a difference, though.

@bohwaz
Copy link
Contributor Author

bohwaz commented Aug 31, 2022

Same issue with POST, yeah, it's probably coming from the stream handling. I read the code, but I'm not sure where the issue might be. I can see that there is already code to read the stream to the end to try to emulate seeking into a stream, but that might not be related.

% curl -v http://localhost:8080/a.php -d 'testtest'
*   Trying 127.0.0.1:8080...                                                            
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /a.php HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Length: 8
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 8 out of 8 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Host: localhost:8080
< Date: Wed, 31 Aug 2022 00:30:18 GMT
< Connection: close
< X-Powered-By: PHP/8.1.9
< Content-type: text/html; charset=UTF-8
< 
bool(true)
int(0)
int(-1)
bool(false)
* Closing connection 0

@iluuu1994
Copy link
Member

I can reproduce this on Fedora and PHP 8.1.

@bukka
Copy link
Member

bukka commented Sep 5, 2022

I did a bit of debugging and it's basically that input stream body is a temp stream which has got inner memory stream. The problem is that memory stream currently fails when trying to seek past end which is the case here - if you add more data, then it is ok (e.g. curl -v http://localhost:8080/a.php -d 'testtestest). There is actually already a bug for that https://bugs.php.net/bug.php?id=52335 so this is kind of a duplicate but will leave it open as it is a bit different from the user point of view...

@bohwaz
Copy link
Contributor Author

bohwaz commented Sep 5, 2022

Ah! Good find! But… Not really :)

Actually what you are referring to works with a longer string and POST request, but il fails with PUT as I originally reported.

% curl http://localhost:8080/a.php -X POST -d testtesttesttest   
bool(true)
int(0)
int(10)

OK that works as you said. But now with PUT:

% curl http://localhost:8080/a.php -X PUT -d testtesttesttest    
bool(true)
int(-1)
bool(false)

As you see, same script, same body data, just a different HTTP request method and it fails.

I tested with other methods, eg. DELETE, SEARCH, etc. and it only works with POST, but not with the other methods.

I found this bug because I wanted to know the length of php://input when storing files from PUT in a WebDAV library, using fseek($a, 0, SEEK_END), it returned 0 (success) but ftell then always returned 0 as well.

@bohwaz bohwaz changed the title fseek does not work with php://input even though the stream is advertised as seekable fseek does not work with php://input and PUT even though the stream is advertised as seekable Sep 5, 2022
@bohwaz
Copy link
Contributor Author

bohwaz commented Sep 5, 2022

I changed the title to reflect that it's not a issue with POST but when using other methods.

@bukka
Copy link
Member

bukka commented Sep 5, 2022

The difference is that POST is handled specially by SAPI where sapi_module_struct read_post callback is called for POST requests so the data are usually pre-read. This is specifically done for $_POST and not needed for that other methods.

Technically it's still a bug even for POST because reading past end should not fail. Obviously it's worse for other methods where the request body is not pre read so you cannot seek even on the supplied data. In any case the core issue is that memory stream fails to seek past end so nothing really changes from my last comment. :)

@bohwaz
Copy link
Contributor Author

bohwaz commented Sep 5, 2022

The difference is that POST is handled specially by SAPI where sapi_module_struct read_post callback is called for POST requests so the data are usually pre-read. This is specifically done for $_POST and not needed for that other methods.

Ah! yes I understand, because POST is pre-read, you can seek, but other requests are not read, so you can't seek unless you read them first. That makes sense.

So yes it's actually two issues if I understand correctly:

  1. php://input is not seekable unless read once before (already done for POST by the SAPI), either it should not report as seekable, or it should be seekable
  2. Reading past the end of a memory stream doesn't have the same behavior as with a file stream

Thanks for your help, I hope we can fix that :)

@bukka bukka changed the title fseek does not work with php://input and PUT even though the stream is advertised as seekable fseek does not work with php://input when data is not pre-read Aug 27, 2023
@bukka bukka self-assigned this Aug 27, 2023
@bukka
Copy link
Member

bukka commented Aug 27, 2023

Fix in #12058

@bukka bukka closed this as completed in ba9650d Aug 28, 2023
@bohwaz
Copy link
Contributor Author

bohwaz commented Aug 28, 2023

Awesome, thank you @bukka :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants