当前位置:WooYun(白帽子技术社区) >> java >> JavaWeb开发安全大家都是怎么处理的?
最近一个人在写一个论坛系统,求共享在现实中的项目的WEB开发安全姿势。
对于常见的SQL注入采用预编译就行了,但是很多时候简单的条件较多或较为复杂的时候很多人都想偷懒拼SQL总是似乎免不了的。
写了个这样的多条件查询条件自动匹配:public static String SQL_FORUM_CLASS_SETTING = "SELECT * from bjcyw_forum_forum where 1=1 ";
public List<Map<String, Object>> getForumClass(Map<String,Object> forum) {
StringBuilder sql=new StringBuilder(SQL_FORUM_CLASS_SETTING);
List<Object> ls=new ArrayList<Object>();
if (forum.size()>0) {
for (String key : forum.keySet()) {
Object obj[]=(Object [])forum.get(key);
sql = SqlHelper.selectHelper(sql, obj);
if ("like".equalsIgnoreCase(obj[2].toString().trim())) {
ls.add("%"+obj[1]+"%");
}else {
ls.add(obj[1]);
}
}
}
return jdbcTemplate.queryForList(sql.toString(),(Object[])ls.toArray());
}public static StringBuilder selectHelper(StringBuilder sql, Object obj[]){
if (Constants.SQL_HELPER_LIKE.equalsIgnoreCase(obj[2].toString())) {
sql.append(" AND "+obj[0]+" like ?");
}else if (Constants.SQL_HELPER_EQUAL.equalsIgnoreCase(obj[2].toString())) {
sql.append(" AND "+obj[0]+" = ?");
}else if (Constants.SQL_HELPER_GREATERTHAN.equalsIgnoreCase(obj[2].toString())) {
sql.append(" AND "+obj[0]+" > ?");
}else if (Constants.SQL_HELPER_LESSTHAN.equalsIgnoreCase(obj[2].toString())) {
sql.append(" AND "+obj[0]+" < ?");
}else if (Constants.SQL_HELPER_NOTEQUAL.equalsIgnoreCase(obj[2].toString())) {
sql.append(" AND "+obj[0]+" != ?");
}
return sql;
}
信任客户端的参数一切参数只匹配查询条件,把参数和条件自动装配到框架。如果客户端提交了危险的SQL也没有关系在query的时候是会预编译。
XSS和Get的参数的简易处理(参数过滤得有点问题,懒得改一般就用下htmlSpecialChars):package net.ltan.bbs.security;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
/**
* 检测用户提交的数据
* @author selina
*
*/
public class CheckRequestData {
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0
|| "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0
|| "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0
|| "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
}
/**
* SQL注入于XSS攻击自动检测
* @param str
* @return
*/
public static boolean checkSqlInjection(String str) {
Logger logger = Logger.getLogger("u3");
String limit = "-囧script囧alert囧hex囧\\(囧\\)囧\\*囧iframe囧\\+囧\\%囧window囧cookie囧and囧or囧net user囧/add囧execute囧into囧outfile囧exec囧net囧select囧count囧char囧insert囧delete囧drop囧from囧master囧truncate囧char囧declare囧or囧by囧;囧#囧%囧`囧:囧\"囧\'囧";
String keys[] = limit.split("囧");
for (int i = 0; i < keys.length; i++) {
Pattern checkSQL = Pattern.compile(keys[i],Pattern.CASE_INSENSITIVE);
Matcher matcherFaild = checkSQL.matcher(str.trim());
if (matcherFaild.find()) {
logger.info("攻击地址:"+str);
return false;
}
}
return true;
}
/**
* 模拟PHP的htmlSpecialChars,替换HTML标记
* @param str
* @return
*/
public static String htmlSpecialChars(String str) {
str = str.replaceAll("&", "&");
str = str.replaceAll("<", "<");
str = str.replaceAll(">", ">");
str = str.replaceAll("\"", """);
str = str.replaceAll("'", "'");
return str;
}
}
防御还得从根入手,JEECMS在这做的就非常不错。是JSP、PHP、ASP的filter让所有的动态脚本请求失效,我们也可以采用类似的手段来保护我们的WEB应用程序。
我这里采用的框架是:Spring3+SpringMVC+FreeMarker。没有采用ORM框架而是用了Spring的jdbcTemplate,缓存采用了memcached(IP限制端口修改)。服务器:apache+nginx+resin3。
展示层采用了htm,对没错只是把freemarker的ftl换做了htm然后在htm里面写freemarker的循环。<!-- FreeMarker 配置 -->
<servlet>
<servlet-name>freemarker</servlet-name>
<servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
<!-- FreemarkerServlet settings: -->
<init-param>
<param-name>TemplatePath</param-name>
<param-value>/</param-value>
</init-param>
<init-param>
<param-name>NoCache</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>ContentType</param-name>
<param-value>text/html</param-value>
</init-param>
<!-- FreeMarker settings: -->
<init-param>
<param-name>template_update_delay</param-name>
<param-value>0</param-value>
<!--
0 is for development only! Use higher value otherwise.
-->
</init-param>
<init-param>
<param-name>default_encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
<init-param>
<param-name>locale</param-name>
<param-value>zh_CN</param-value>
</init-param>
<init-param>
<param-name>number_format</param-name>
<param-value>0.##########</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>freemarker</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
控制层的配置:<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>*.shmtl</url-pattern>
<url-pattern>*.php</url-pattern>
<url-pattern>*.phpx</url-pattern>
</servlet-mapping>
很明显我们的一切的shmtl(特殊业务保留后缀)、php(前台的普通业务请求后缀)、phpx(后台的一般请求后缀)都会被Spring处理。URL授权和业务逻辑权限控制。也许用户在前台会看到这样的连接:http://ltan.net/index.php。但是这并不是请求了服务器的index.php文件而是Controller,采用了kindEditor,简单改改就行了:package net.ltan.bbs.app.editor;
import java.util.*;
import java.io.*;
import java.text.SimpleDateFormat;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.*;
import org.apache.commons.fileupload.servlet.*;
import org.json.simple.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* kindEditor 编辑器文件上传
* @author selina
*
*/
@SuppressWarnings("unchecked")
@Controller
public class KindEditorUploadUtil {
@RequestMapping("/editor/upload_json.php")
public void upload(HttpServletRequest request, HttpServletResponse response)
throws IOException, FileUploadException {
PrintWriter out = response.getWriter();
// 文件保存目录路径
String savePath = request.getSession().getServletContext().getRealPath(
"/")
+ "uploads/";
// 文件保存目录URL
String saveUrl = request.getContextPath() + "/uploads/";
// 定义允许上传的文件扩展名
HashMap<String, String> extMap = new HashMap<String, String>();
extMap.put("image", "gif,jpg,jpeg,png,bmp");
extMap.put("flash", "swf,flv");
extMap.put("media", "swf,flv,mp3,wav,wma,wmv,mid,avi,mpg,asf,rm,rmvb");
extMap.put("file", "doc,docx,xls,xlsx,ppt,htm,html,txt,zip,rar,gz,bz2");
// 最大文件大小
long maxSize = 1000000;
response.setContentType("text/html; charset=UTF-8");
if (!ServletFileUpload.isMultipartContent(request)) {
out.println(getError("请选择文件。"));
return;
}
// 检查目录
File uploadDir = new File(savePath);
if (!uploadDir.isDirectory()) {
out.println(getError("上传目录不存在。"));
return;
}
// 检查目录写权限
if (!uploadDir.canWrite()) {
out.println(getError("上传目录没有写权限。"));
return;
}
String dirName = request.getParameter("dir");
if (dirName == null) {
dirName = "image";
}
if (!extMap.containsKey(dirName)) {
out.println(getError("目录名不正确。"));
return;
}
// 创建文件夹
savePath += dirName + "/";
saveUrl += dirName + "/";
File saveDirFile = new File(savePath);
if (!saveDirFile.exists()) {
saveDirFile.mkdirs();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String ymd = sdf.format(new Date());
savePath += ymd + "/";
saveUrl += ymd + "/";
File dirFile = new File(savePath);
if (!dirFile.exists()) {
dirFile.mkdirs();
}
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");
List items = upload.parseRequest(request);
Iterator itr = items.iterator();
while (itr.hasNext()) {
FileItem item = (FileItem) itr.next();
String fileName = item.getName();
if (!item.isFormField()) {
// 检查文件大小
if (item.getSize() > maxSize) {
out.println(getError("上传文件大小超过限制。"));
return;
}
// 检查扩展名
String fileExt = fileName.substring(
fileName.lastIndexOf(".") + 1).toLowerCase();
if (!Arrays.<String> asList(extMap.get(dirName).split(","))
.contains(fileExt)) {
out.println(getError("上传文件扩展名是不允许的扩展名。\n只允许"
+ extMap.get(dirName) + "格式。"));
return;
}
SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
String newFileName = df.format(new Date()) + "_"
+ new Random().nextInt(1000) + "." + fileExt;
try {
File uploadedFile = new File(savePath, newFileName);
item.write(uploadedFile);
} catch (Exception e) {
out.println(getError("上传文件失败。"));
return;
}
JSONObject obj = new JSONObject();
obj.put("error", 0);
obj.put("url", saveUrl + newFileName);
out.println(obj.toJSONString());
}
}
}
private String getError(String message) {
JSONObject obj = new JSONObject();
obj.put("error", 1);
obj.put("message", message);
return obj.toJSONString();
}
}package net.ltan.bbs.app.editor;
import java.util.*;
import java.io.*;
import java.text.SimpleDateFormat;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileUploadException;
import org.json.simple.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* kindEditor 编辑器文件管理
* @author selina
*
*/
@SuppressWarnings("unchecked")
@Controller
public class KindEditorFileManager {
@RequestMapping("/editor/file_manager_json.php")
public void fileManager(HttpServletRequest request,
HttpServletResponse response) throws IOException,
FileUploadException {
PrintWriter out = response.getWriter();
// 根目录路径,可以指定绝对路径,比如 /var/www/attached/
String rootPath = request.getSession().getServletContext().getRealPath(
"/")
+ "uploads/";
// 根目录URL,可以指定绝对路径,比如 http://www.yoursite.com/attached/
String rootUrl = request.getContextPath() + "/uploads/";
// 图片扩展名
String[] fileTypes = new String[] { "gif", "jpg", "jpeg", "png", "bmp" };
String dirName = request.getParameter("dir");
if (dirName != null) {
if (!Arrays.<String> asList(
new String[] { "image", "flash", "media", "file" })
.contains(dirName)) {
out.println("Invalid Directory name.");
return;
}
rootPath += dirName + "/";
rootUrl += dirName + "/";
File saveDirFile = new File(rootPath);
if (!saveDirFile.exists()) {
saveDirFile.mkdirs();
}
}
// 根据path参数,设置各路径和URL
String path = request.getParameter("path") != null ? request
.getParameter("path") : "";
String currentPath = rootPath + path;
String currentUrl = rootUrl + path;
String currentDirPath = path;
String moveupDirPath = "";
if (!"".equals(path)) {
String str = currentDirPath.substring(0,
currentDirPath.length() - 1);
moveupDirPath = str.lastIndexOf("/") >= 0 ? str.substring(0, str
.lastIndexOf("/") + 1) : "";
}
// 排序形式,name or size or type
String order = request.getParameter("order") != null ? request
.getParameter("order").toLowerCase() : "name";
// 不允许使用..移动到上一级目录
if (path.indexOf("..") >= 0) {
out.println("Access is not allowed.");
return;
}
// 最后一个字符不是/
if (!"".equals(path) && !path.endsWith("/")) {
out.println("Parameter is not valid.");
return;
}
// 目录不存在或不是目录
File currentPathFile = new File(currentPath);
if (!currentPathFile.isDirectory()) {
out.println("Directory does not exist.");
return;
}
// 遍历目录取的文件信息
List<Hashtable> fileList = new ArrayList<Hashtable>();
if (currentPathFile.listFiles() != null) {
for (File file : currentPathFile.listFiles()) {
Hashtable<String, Object> hash = new Hashtable<String, Object>();
String fileName = file.getName();
if (file.isDirectory()) {
hash.put("is_dir", true);
hash.put("has_file", (file.listFiles() != null));
hash.put("filesize", 0L);
hash.put("is_photo", false);
hash.put("filetype", "");
} else if (file.isFile()) {
String fileExt = fileName.substring(
fileName.lastIndexOf(".") + 1).toLowerCase();
hash.put("is_dir", false);
hash.put("has_file", false);
hash.put("filesize", file.length());
hash.put("is_photo", Arrays.<String> asList(fileTypes)
.contains(fileExt));
hash.put("filetype", fileExt);
}
hash.put("filename", fileName);
hash.put("datetime",
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(file
.lastModified()));
fileList.add(hash);
}
}
if ("size".equals(order)) {
Collections.sort(fileList, new SizeComparator());
} else if ("type".equals(order)) {
Collections.sort(fileList, new TypeComparator());
} else {
Collections.sort(fileList, new NameComparator());
}
JSONObject result = new JSONObject();
result.put("moveup_dir_path", moveupDirPath);
result.put("current_dir_path", currentDirPath);
result.put("current_url", currentUrl);
result.put("total_count", fileList.size());
result.put("file_list", fileList);
response.setContentType("application/json; charset=UTF-8");
out.println(result.toJSONString());
}
public class NameComparator implements Comparator {
public int compare(Object a, Object b) {
Hashtable hashA = (Hashtable) a;
Hashtable hashB = (Hashtable) b;
if (((Boolean) hashA.get("is_dir"))
&& !((Boolean) hashB.get("is_dir"))) {
return -1;
} else if (!((Boolean) hashA.get("is_dir"))
&& ((Boolean) hashB.get("is_dir"))) {
return 1;
} else {
return ((String) hashA.get("filename"))
.compareTo((String) hashB.get("filename"));
}
}
}
public class SizeComparator implements Comparator {
public int compare(Object a, Object b) {
Hashtable hashA = (Hashtable) a;
Hashtable hashB = (Hashtable) b;
if (((Boolean) hashA.get("is_dir"))
&& !((Boolean) hashB.get("is_dir"))) {
return -1;
} else if (!((Boolean) hashA.get("is_dir"))
&& ((Boolean) hashB.get("is_dir"))) {
return 1;
} else {
if (((Long) hashA.get("filesize")) > ((Long) hashB
.get("filesize"))) {
return 1;
} else if (((Long) hashA.get("filesize")) < ((Long) hashB
.get("filesize"))) {
return -1;
} else {
return 0;
}
}
}
}
public class TypeComparator implements Comparator {
public int compare(Object a, Object b) {
Hashtable hashA = (Hashtable) a;
Hashtable hashB = (Hashtable) b;
if (((Boolean) hashA.get("is_dir"))
&& !((Boolean) hashB.get("is_dir"))) {
return -1;
} else if (!((Boolean) hashA.get("is_dir"))
&& ((Boolean) hashB.get("is_dir"))) {
return 1;
} else {
return ((String) hashA.get("filetype"))
.compareTo((String) hashB.get("filetype"));
}
}
}
}
数据库直接采用的是DZ,所以提供了几个普遍的加密:package net.ltan.bbs.security;
import java.util.HashMap;
import java.util.Map;
import net.ltan.bbs.util.StringUtil;
import org.apache.commons.codec.digest.DigestUtils;
/**
* 加密业务
* @author selina
*
*/
public class Encryption {
/**
* 实现Discuz用户加密算法
* @author Administrator
*
*/
public static Map<String,String> Encryptions(String context)
{
String salt=StringUtil.randomString(6);
String password=DigestUtils.md5Hex(DigestUtils.md5Hex(context)+salt).toString();
Map<String,String> encryption=new HashMap<String, String>();
encryption.put("salt",salt);
encryption.put("password",password);
return encryption;
}
public static Map<String,String> Encryptions(String context,String salt)
{
String password=DigestUtils.md5Hex(DigestUtils.md5Hex(context)+salt).toString();
Map<String,String> encryption=new HashMap<String, String>();
encryption.put("salt",salt);
encryption.put("password",password);
return encryption;
}
/**
* 管理员密码加密
* @param context
* @return
*/
public static String AdminPassWordEncryptions(String context)
{
StringBuffer sb=new StringBuffer();
String md5=DigestUtils.md5Hex(context+"ltan");
return sb.append(md5).reverse().delete(3,6).delete(13,19).delete(2,9).toString();
}
}
异常处理就丢log就行了,界面最好不要出现报错。log4j.appender.a_manage.File=${catalina.home}/logs/lt_manage.log
log4j.appender.a_forum.File=${catalina.home}/logs/lt_forum.log
log4j.appender.a_spr.File=${catalina.home}/logs/spring.log
log4j.appender.a_lt.File=${catalina.home}/logs/lt.log
XSS危险确实非常大,用户认证session+IP。