前言,
有几个月没写php了. 刚好公司有个web的项目需要改下之前他们搭建的Wordpress的站点, 因为给所有的page/post都加上了权限组来控制权限和SSO登录认证一体化. 但是附件却没有保护到. Wordpress直接暴露了附件的服务器上的地址.作为一个安全隐患, 这个需要修复.
本文适合有一些WP的插件开发经验的朋友.
1,数据准备,
WP的附件attachment存放在Wordpress的后台的Media这个页面来控制,也可以直接通过发page的时候添加媒体文件来实现.
数据库存放在wp_posts 和wp_post_meta, 字段是`post_type`=‘attachment‘
也就是每当你需要发一篇文章的时候, 当你点击了一次上传媒体文件, Wordpress会存一个post到wp_posts表和wp_post_meta表. 一个文件对应一次数据的插入.
其中wp_post_meta表有4个字段, 分别是meta_id, post_id, meta_key, meta_value
meta_key=‘_wp_attached_file‘ 的数据就是我们的媒体文件了, 对应这条数据的meta_value是一个包含了上传时间和文件名的字符串.
而在wp_posts表, 对应的post_id的就是这个媒体文件的post的数据, 这里面我们最主要关心的是post_type字段和post_mine_type字段. 这里我上传的是一个mp3文件, 所以
对应的数据如下.
2, 思路拓展
要改变文章里面的数据, 就有几种方案. 通过上面的数据, 可以看见Wordpress是把一个相对或者绝对的路径存到了数据库. 这个地址就暴露在了外面. 在发文章的时候, Wordpress同样也存入了这个地址.
假定我们的域名是cnwp.com, 现在想实现的是所有的附件链接都最终指向cnwp.com/download.php?fdi=xxxx来控制文件下载.
这导致了如果你只修改了media的相关数据, 页面page里面存入的链接是已经保存下来的, 没有被改掉.
通过阅读源码, 发现Wordpress的insert post在插入post数据的时候, 如果判断到有attachment附件的数据插入, 就会提前去调wp_insert_attachment的函数. 所以这里可以使用apply_filter来过滤insert的数据. 但是我们是在插件里面来调, 钩子在获取数据的时候数据已经提前插入了. Wordpress没有提供修改插入附件的filter给我们使用. 这样修改了原始的数据, 实现是比较麻烦和复杂. 这条路比较难. 现卡在这里. 于是又去阅读了一下Wordpress其他的下载管理的插件,
做的比较好的是WPDM, download manager这个插件, 但是这个插件最终也是暴露了实际的下载地址. 它只是把按照日期目录排的文件移到了它自定的一个位置. 实际是一样的. 还是收费的. 而且对于之前的权限组不能控制到这个链接. 只控制到了post的content的数据. 所以也放弃.
查阅到了the_content和wp_get_attachment_url 两个filter, 一个是在控制page的content内容, 一个是控制attachment附件页面的url.
前面说过, WP每次在上传附件的时候, 实际上也发了一条post的数据, 那么既然是post的数据, 就有一个地址, 这个地址就是 http://cnwp.com/xxx/?attachment_id=103 类似的地址,xxx是network sites(WP的多站点子目录模式, 还有多站点子域名模式, 类似xxx.cnwp.com), 这个地址就是attachment附件的页面.
所以我们要改的就是改掉页面上所有attachment的链接, 不管它在一个页面里面是以http://cnwp.com/xxx/?attachment_id=xxx的形式出现,
还是以http://cnwp.com/xxx/wp-content/uploads/sites/6/2015/01/Yanni-Ninghtingale.mp3 这样的链接出现.
我们要保证后一个url的实际的文件地址,不会出现在用户的面前.
3, 首先去插件页面, 加filter,
array($sc, ‘scAttachmentRewrite‘) 这个是调的我的一个类, 里面有一个scAttachmentRewrite的函数, 后面的10是优先级, 1是the_content会传入进scAttachmentRewrite函数的参数
同理,scAttachmentUrlRewrite也是类似的一个.
再来看下函数. 这个函数是来重写the_content的url的, 通过正则来分段匹配出数据. 然后获取到页面的post的id, 这个post_id是和相关的权限组作了绑定, 这里我取出来是为了方便后面处理. 通过scUpdateAttachUrl函数来更新或者插入一个自定义的关联表, 里面存的是一会儿要重写的url的id, 这样就可以通过这个id来作新的替换掉的id的值
/** * Rewrite the content attachment links. * * @param string $content theContent. * * @return mix. */ public function scAttachmentRewrite($content) { $pattern = "/<a href=\"(\S+)(\/wp-content\/uploads\/sites\/)([0-9]+)(\S+)\">/"; preg_match_all($pattern, $content, $match); // $match[0] all links // $match[1] link head http://cnwp.com/xxx or http://xxx.cnwp.com // $match[2] replace content /wp-content/uploads/sites/ // $match[3] site num // $match[4] file meta_value if (isset($match) && is_array($match)) { $post_id = get_the_ID(); $old_links = array(); $new_links = array(); foreach ($match[1] as $k => $v) { $old_links[$k] = $v.$match[2][$k].$match[3][$k].$match[4][$k]; $_snx_id = $this->scUpdateAttachUrl($match[3][$k], $match[4][$k]); $rand_string = $this->scRandString(10); // s=site no, id=sc attachment_id, p=page/post id $param = $rand_string.base64_encode('s='.$match[3][$k].'&id='.$_snx_id.'&p='.$post_id); $new_links[$k] = $v.'/sc_download.php?fid='.$param; } array_walk($old_links, array($this, 'scArrayWalks')); $content = preg_replace($old_links, $new_links, $content); } return $content; }
scAttachmentUrlRewrite处理的流程和下面的函数基本类似. 唯一不同的是因为是单个附件的page, 所以需要处理掉图片,音频等直接呈现在用户面前的url, 保证它们不会被重写掉. 所以就需要用获取到的post_id和这个attachment的页面的数据去查询post表里面post_mime_type=attachment的数据,排除掉media,png,gif等的数据.保证只重写对应格式的.
其中的代码片段, 其他和上面一样, 不过wp_get_attachment_url传入的参数是一个url地址, 正则就稍微不一样一点儿,
$pattern = "/(\S+)(\/wp-content\/uploads\/sites\/)([0-9]+)(\S+)/";
scQueryMediaMeta就是去查询的对应的post数据. 进而用post_mime_type来对比. 如果有其他格式的, 大家也可以自己加对应的
$checkMedia = $this->scQueryMediaMeta($match[3], $match[4], $post_id); if (!$checkMedia) { return $url; } else { if (isset($checkMedia['post_type']) && $checkMedia['post_type'] == 'attachment' && isset($checkMedia['post_mime_type']) ) { $checkApp = strpos($checkMedia['post_mime_type'], 'application'); if ($checkApp === false) { return $url; } else { $_snx_id = $this->scUpdateAttachUrl($match[3], $match[4]); $rand_string = $this->scRandString(10); // s=site no, id=sc attachment_id, aid=post type attachment type id $param = $rand_string.base64_encode('s='.$match[3].'&id='.$_snx_id.'&a='.$post_id); // return $match[1].'/sc_download.php?s='.$match[3].'&id='.$_snx_id; return $match[1].'/sc_download.php?fid='.$param; } } else { return $url; } }
这样重写完, 页面输出的url就已经变掉了.
解下来是sc_download.php文件. 因为我把传入的参数base64加密了一次,并且混入了10个随机的字符,保证url的唯一和随机性. 所以这里要处理下这个逻辑,把原来的参数要处理解析出来.
其他没什么好说的了, 唯一就是header的转发, 在header的Content-Disposition : attachment 无效, WP屏蔽了直接附件的下载. 所以修改下, 修改后完整的header下载是这样
其中$file_path就是实际的文件路径. 需要加上status_header(200)才能保证attachment作为附件的方式下载, 否则就只有浏览器直接打开的inline方式了.
在header发送前就可以作权限处理.模板之类的数据处理. 这里就省略掉了.
@ob_end_clean(); status_header(200); header("Pragma: public"); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); header("Cache-Control: private", false); if (!(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on" && preg_match("/MSIE/", $_SERVER["HTTP_USER_AGENT"]))) { header('Pragma: no-cache'); } header("Content-Type: application/octet-stream"); header("Content-Type: application/force-download"); header("Content-Type: ".mime_content_type($file_path)); header("Content-Disposition: attachment; filename=\"".urlencode(basename($file_path))."\""); //header("Content-Disposition: inline; filename=\"".urlencode(basename($file_path))."\";"); header("Content-Transfer-Encoding: binary"); header("Content-Length: ".filesize($file_path)); ob_clean(); flush(); readfile("$file_path"); exit;