通过PHP或Apache打破服务器端的HTTP文件上传(Break HTTP file upload

2019-08-31 21:57发布

当上传大文件(> 100米)到服务器,PHP总是先接受来自浏览器的整个数据POST。 我们不能注入上传的过程。

例如,检查“的值, token ”之前整个数据发送到服务器,在我的PHP代码不可能的

<form enctype="multipart/form-data" action="upload.php?token=XXXXXX" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000" />
    Send this file: <input name="userfile" type="file" />
    <input type="submit" value="Send File" />
</form>

所以,我尝试使用mod_rewrite是这样的:

RewriteEngine On
RewriteMap mymap prg:/tmp/map.php
RewriteCond %{QUERY_STRING} ^token=(.*)$ [NC]
RewriteRule ^/upload/fake.php$ ${mymap:%1} [L]

map.php

#!/usr/bin/php
<?php
define("REAL_TARGET", "/upload/real.php\n");
define("FORBIDDEN", "/upload/forbidden.html\n");

$handle = fopen ("php://stdin","r");
while($token = trim(fgets($handle))) {
file_put_contents("/tmp/map.log", $token."\n", FILE_APPEND);
    if (check_token($token)) {
        echo REAL_TARGET;
    } else {
        echo FORBIDDEN;
    }
}

function check_token ($token) {//do your own security check
    return substr($token,0,4) === 'alix';
}

但是......它再次失败。 mod_rewrite看起来在这种情况下也工作到很晚。 数据还完全转移。

然后我试图Node.js ,像这样的(代码SNIP):

var stream = new multipart.Stream(req);
stream.addListener('part', function(part) {
    sys.print(req.uri.params.token+"\n");
    if (req.uri.params.token != "xxxx") {//check token
      res.sendHeader(200, {'Content-Type': 'text/plain'});
      res.sendBody('Incorrect token!');
      res.finish();
      sys.puts("\n=> Block");
      return false;
    }

结果...... 再次失败。

所以,请帮我找到解决这个问题,或者告诉我没有办法正确的路径。

相关的问题:

PHP(与Apache或Nginx的)POST请求前检查HTTP标头可以完成?

可一些告诉我如何使输入密码这个脚本检查它开始上传过程,而不是文件上传后的前?

Answer 1:

首先, 你可以使用GitHub库我为此创建自己试试这个代码 。 只是克隆库和运行node header

(扰流板,如果你正在读这篇文章,是在时间压力下得到的东西的工作,并没有心情学习(:(),有一个简单的解决方案在最后)

总体思路

这是一个很好的问题。 你所要求的是非常可能的不需要客户方 ,只是一个HTTP协议是如何工作的更深层次的理解,同时展示了如何Node.js的岩石:)

这可以变得容易,如果我们去更深一层底层TCP协议和处理HTTP请求自己的这种特殊情况下。 Node.js的可以让你做到这一点很容易地使用内置的网络模块 。

HTTP协议

首先,让我们来看看在HTTP请求中是如何工作的。

一个HTTP请求包括在关键的一般格式的标头部分的:由CRLF(分隔值对\r\n )。 我们知道,当我们到达一个双CRLF结束了头部分(即\r\n\r\n )。

一个典型的HTTP GET请求可能会是这个样子:

GET /resource HTTP/1.1  
Cache-Control: no-cache  
User-Agent: Mozilla/5.0 

Hello=World&stuff=other

的“空”线之前的顶部部分是标头部分和底部部分是请求的主体。 您的请求将看起来有点不同的身体部分,因为它是与编码multipart/form-data ,但头仍然similarLet的探讨如何适用于我们。

在TCP的NodeJS

我们可以听在TCP的原始请求和阅读我们得到的,直到我们看到双CRLF我们谈到的数据包。 然后,我们会检查我们已经为我们所需要的任何验证的短头部分。 我们这样做了之后,我们可以终止该请求,如果验证未通过(例如,通过简单地结束TCP连接),或让它通过。 这让我们不接收或读取请求主体,而只是头这小得多。

一个简单的方法嵌入到现有的应用程序,这是由它请求代理到实际的HTTP服务器的具体使用情况。

实施细节

该解决方案是因为它得到裸露的骨头 。 这仅仅是一个建议。

这里是工作流程:

  1. 我们要求net在Node.js的模块,它允许我们创建在node.js中的TCP服务器

  2. 创建一个使用TCP服务器net模块,它会听数据: var tcpServer = net.createServer(function (socket) {...不要忘记告诉它来听正确的端口

    • 内部的回调,听数据事件socket.on("data",function(data){ ,每当一个数据包到达将触发。
    • 从“数据”事件读出的传递缓冲的数据,并存储在一个变量
    • 检查双CRLF,这确保了请求HEADER部分已经结束根据HTTP协议
    • 假设验证头(令牌,你的话)检查解析只是头后,(也就是,我们得到了双CRLF)。 为Content-Length头检查时,这也适用。
    • 如果您发现该头查不出来,叫socket.end()将关闭连接。

这里有一些事情,我们将使用

一种用于读取头的方法:

function readHeaders(headers) {
    var parsedHeaders = {};
    var previous = "";    
    headers.forEach(function (val) {
        // check if the next line is actually continuing a header from previous line
        if (isContinuation(val)) {
            if (previous !== "") {
                parsedHeaders[previous] += decodeURIComponent(val.trimLeft());
                return;
            } else {
                throw new Exception("continuation, but no previous header");
            }
        }

        // parse a header that looks like : "name: SP value".
        var index = val.indexOf(":");

        if (index === -1) {
            throw new Exception("bad header structure: ");
        }

        var head = val.substr(0, index).toLowerCase();
        var value = val.substr(index + 1).trimLeft();

        previous = head;
        if (value !== "") {
            parsedHeaders[head] = decodeURIComponent(value);
        } else {
            parsedHeaders[head] = null;
        }
    });
    return parsedHeaders;
};

一种用于缓冲你会得到一个数据事件,如果它在一个对象存在返回其位置检查双CRLF方法:

function checkForCRLF(data) {
    if (!Buffer.isBuffer(data)) {
        data = new Buffer(data,"utf-8");
    }
    for (var i = 0; i < data.length - 1; i++) {
        if (data[i] === 13) { //\r
            if (data[i + 1] === 10) { //\n
                if (i + 3 < data.length && data[i + 2] === 13 && data[i + 3] === 10) {
                    return { loc: i, after: i + 4 };
                }
            }
        } else if (data[i] === 10) { //\n

            if (data[i + 1] === 10) { //\n
                return { loc: i, after: i + 2 };
            }
        }
    }    
    return { loc: -1, after: -1337 };
};

而这个小工具的方法:

function isContinuation(str) {
    return str.charAt(0) === " " || str.charAt(0) === "\t";
}

履行

var net = require("net"); // To use the node net module for TCP server. Node has equivalent modules for secure communication if you'd like to use HTTPS

//Create the server
var server = net.createServer(function(socket){ // Create a TCP server
    var req = []; //buffers so far, to save the data in case the headers don't arrive in a single packet
    socket.on("data",function(data){
        req.push(data); // add the new buffer
        var check = checkForCRLF(data);
        if(check.loc !== -1){ // This means we got to the end of the headers!
            var dataUpToHeaders= req.map(function(x){
                return x.toString();//get buffer strings
            }).join("");
            //get data up to /r/n
            dataUpToHeaders = dataUpToHeaders.substring(0,check.after);
            //split by line
            var headerList = dataUpToHeaders.trim().split("\r\n");
            headerList.shift() ;// remove the request line itself, eg GET / HTTP1.1
            console.log("Got headers!");
            //Read the headers
            var headerObject = readHeaders(headerList);
            //Get the header with your token
            console.log(headerObject["your-header-name"]);

            // Now perform all checks you need for it
            /*
            if(!yourHeaderValueValid){
                socket.end();
            }else{
                         //continue reading request body, and pass control to whatever logic you want!
            }
            */


        }
    });
}).listen(8080); // listen to port 8080 for the sake of the example

如果您有任何问题随时问 :)

好吧,我撒谎了,还有一个更简单的方式!

但是,什么是在的乐趣? 如果您最初跳过这里,你不会学习如何HTTP工程:)

Node.js的有一个内置的http模块。 由于请求是由性质node.js的分块,特别长的请求,就可以实现同样的事情,如果没有该协议的更深入的了解。

这一次,让我们使用http模块创建HTTP服务器

server = http.createServer( function(req, res) { //create an HTTP server
    // The parameters are request/response objects
    // check if method is post, and the headers contain your value.
    // The connection was established but the body wasn't sent yet,
    // More information on how this works is in the above solution
    var specialRequest = (req.method == "POST") && req.headers["YourHeader"] === "YourTokenValue";
    if(specialRequest ){ // detect requests for special treatment
      // same as TCP direct solution add chunks
      req.on('data',function(chunkOfBody){
              //handle a chunk of the message body
      });
    }else{
        res.end(); // abort the underlying TCP connection, since the request and response use the same TCP connection this will work
        //req.destroy() // destroy the request in a non-clean matter, probably not what you want.
    }
}).listen(8080);

这是基于以下事实的request一中的NodeJS手柄http模块实际上是挂钩的头部被默认发出后(但执行没有别的)。 (此服务器模块中 , 该解析器模块中)

用户igorw建议使用稍微清洁的解决方案100 Continue头假设浏览器你的目标支持它。 100继续是一个状态代码设计做你尝试什么:

的100(继续)状态的目的(参见10.1.1节)是允许客户端在发送请求消息,请求主体,以确定源服务器愿意接受请求(基于请求头)客户端之前将请求发送体。 在某些情况下,可能要么是不恰当或高度低效客户端发送的身体,如果服务器将拒绝该邮件不看身体。

这里是 :

var http = require('http');

function handle(req, rep) {
    req.pipe(process.stdout); // pipe the request to the output stream for further handling
    req.on('end', function () {
        rep.end();
        console.log('');
    });
}

var server = new http.Server();

server.on('checkContinue', function (req, rep) {
    if (!req.headers['x-foo']) {
        console.log('did not have foo');
        rep.writeHead(400);
        rep.end();
        return;
    }

    rep.writeContinue();
    handle(req, rep);
});

server.listen(8080);

你可以看到样品的输入/输出这里 。 这需要你的要求在火中相应的Expect:头。



Answer 2:

使用JavaScript。 通过AJAX提交预成型时,用户点击提交,等待Ajax响应,那么当它回来了成功与否,提交实际的表单。 您也可以回退到你不想这是聊胜于无的方法。

<script type="text/javascript">
function doAjaxTokenCheck() {
    //do ajax request for tokencheck.php?token=asdlkjflgkjs
    //if token is good return true
    //else return false and display error
}
</script>

<form enctype="multipart/form-data" action="upload.php?token=XXXXXX" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000" />
    Send this file: <input name="userfile" type="file" />
    <input type="submit" value="Send File" onclick="return doAjaxTokenCheck()"/>
</form>


Answer 3:

这听起来像你想流的上传和需要处理之前验证:这是否帮助? http://debuggable.com/posts/streaming-file-uploads-with-node-js:4ac094b2-b6c8-4a7f-bd07-28accbdd56cb

http://www.componentix.com/blog/13/file-uploads-using-nodejs-once-again



Answer 4:

我建议你使用一些客户端插件上传文件。 你可以使用

http://www.plupload.com/

要么

https://github.com/blueimp/jQuery-File-Upload/

这两个插件有规定上传前检查文件的大小。

如果你想使用自己的脚本,检查。 这可能会帮助你

        function readfile()
        {
            var files = document.getElementById("fileForUpload").files;
            var output = [];
            for (var i = 0, f; f = files[i]; i++) 
            {
                    if(f.size < 100000) // Check file size of file
                    {
                        // Your code for upload
                    }
                    else
                    {
                        alert('File size exceeds upload size limit');
                    }

            }
        }


Answer 5:

一个版本是有些模糊。 所以我重写了代码来显示路线处理和中间件之间的差异。 中间件是为每个请求执行。 他们是在他们给出的顺序执行。 express.bodyParser()是处理文件的上传,你应该跳过,不正确的令牌中间件。 mymiddleware简单地检查令牌和终止无效的请求。 这必须前完成express.bodyParser()被执行。

var express = require('express'),
app = express();

app.use(express.logger('dev'));
app.use(mymiddleware);                                 //This will work for you.
app.use(express.bodyParser());                         //You want to avoid this
app.use(express.methodOverride());
app.use(app.router);

app.use(express.static(__dirname+'/public'));
app.listen(8080, "127.0.0.1");

app.post('/upload',uploadhandler);                     //Too late. File already uploaded

function mymiddleware(req,res,next){                   //Middleware
    //console.log(req.method);
    //console.log(req.query.token);
    if (req.method === 'GET')
        next();
    else if (req.method === 'POST' && req.query.token === 'XXXXXX')
        next();
    else
        req.destroy();
}

function uploadhandler(req,res){                       //Route handler
    if (req.query.token === 'XXXXXX')
        res.end('Done');
    else
        req.destroy();
}

uploadhandler因为它已被处理,另一方面不能中断上传express.bodyParser()了。 它只是处理POST请求。 希望这可以帮助。



Answer 6:

绕过PHP的后处理的方法之一是借道PHP-CLI的请求。 创建下面的CGI脚本并尝试上传大文件到它。 Web服务器应通过杀死连接响应。 如果确实如此,那么它只是一个打开内部套接字连接,并发送数据到实际位置的事情 ​​- 只要条件满足,当然。

#!/usr/bin/php
<?php

echo "Status: 500 Internal Server Error\r\n";
echo "\r\n";
die();

?>


Answer 7:

你为什么不只是使用APC文件上传进度,因此在这种情况下,提交表单设置进度密钥,作为APC文件上传密钥和上传进度将开始,但随后开始在第一进度检查你将验证密钥如果它不正确,你将中断一切:

http://www.johnboy.com/blog/a-useful-php-file-upload-progress-meter http://www.ultramegatech.com/2008/12/creating-upload-progress-bar-php/

这是做的更天然的方法。 大致相同,只是改变了隐藏输入的关键是你的令牌和验证,并中断了错误的情况下连接。 也许这就是更好的。 http://php.net/manual/en/session.upload-progress.php



文章来源: Break HTTP file uploading from server side by PHP or Apache