1、XSS和HTMl注入
原理:使用一些script脚本和html标签注入进系统,然后进行侵入。
例如:
<img onerror="alert(1)" src="aaa" />
<p><img src=1 onerror=alert("xss") /></p>
<p><a href="http://www.baidu.com:">aaa</a></p>
处理方式:
1、进行转义,可以使用阿帕奇包里的StringEscapeUtils.escapeHtml方法进行字符串转义。
s= StringEscapeUtils.escapeHtml4(s)
2、通过字符串替换进行过滤,替换里面的一些事件标签或者一些脚本标签。
s = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);cleanValue = s.matcher(cleanValue).replaceAll("");s = Pattern.compile("onerror(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);cleanValue = s.matcher(cleanValue).replaceAll("");
3、可以使用Antisamy工具进行统一的数据清洗,不过需要添加一些配置。使用的是antisamy-ebay.xml文件。需要将其放到
Spring mvc版本:
pom文件:排除slf4j是因为对项目产生了jar包冲突,若未产生则不需要排除。
<antisamy.version>1.6.2</antisamy.version>
<!-- https://mvnrepository.com/artifact/org.owasp.antisamy/antisamy -->
<dependency><groupId>org.owasp.antisamy</groupId><artifactId>antisamy</artifactId><version>${antisamy.version}</version><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></exclusion><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId></exclusion></exclusions>
</dependency>
XssFilter.java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;/****Xss过滤器**/
public class XssFilter implements Filter{/*** 换行标识*/public static final String line_flag = "[~line_flag~]";@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// 拦截请求,处理XSS过滤chain.doFilter(new XSSHttpServletRequestWrapper((HttpServletRequest) request), response);}@Overridepublic void destroy() {}
}
XSSHttpServletRequestWrapper.java:getInputStream() 重写此方法是为了获取post提交的json数据–@RequestBody
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;public class XSSHttpServletRequestWrapper extends HttpServletRequestWrapper {public XSSHttpServletRequestWrapper(HttpServletRequest request) {super(request);}/*** 方法说明:过滤掉字符** @param name* @return*/@Overridepublic String getParameter(String name) {String value = super.getParameter(name);return htmlFilter(value);}/*** 方法说明:过滤掉字符** @param name* @return*/@Overridepublic String getHeader(String name) {return htmlFilter(super.getHeader(name));}/*** 方法说明:过滤掉字符** @param name* @return*/@Overridepublic String[] getParameterValues(String name) {String[] values = super.getParameterValues(name);if (values == null || values.length == 0) {return values;}for (int i = 0; i < values.length; i++) {values[i] = htmlFilter(values[i]);}return values;}@Override@SuppressWarnings("unchecked")public Enumeration<String> getParameterNames(){Enumeration<String> e = super.getParameterNames();Vector<String> v = new Vector<String>();while (e.hasMoreElements()) {String paramName = e.nextElement();if(StringUtils.isBlank(paramName)){paramName = "";}paramName = htmlFilter(paramName);v.add(paramName);}return v.elements();}/*** 方法说明:过滤掉字符,struts2获取request参数就是通过该方法,所以以后要注意** @return Map*/@Override@SuppressWarnings("unchecked")public Map<String, String[]> getParameterMap() {Map<String, String[]> returnMap = new HashMap<String, String[]>();Enumeration<String> e = super.getParameterNames();while (e.hasMoreElements()) {String paramName = e.nextElement();String[] values = getParameterValues(paramName);if (StringUtils.isNotBlank(paramName)) {returnMap.put(paramName, values);}}return returnMap;}@Overridepublic ServletInputStream getInputStream() throws IOException {// 非json类型,直接返回if (!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {return super.getInputStream();}String json = IoUtil.read(super.getInputStream(), "utf-8");if (StrUtil.isEmpty(json)) {return super.getInputStream();}//转义json = StringEscapeUtils.unescapeHtml4(json);// 这里要注意,json格式的参数不能直接使用hutool的EscapeUtil.escape, 因为它会把"也给转义,// 使得@RequestBody没办法解析成为一个正常的对象,所以我们自己实现一个过滤方法// 或者采用定制自己的objectMapper处理json出入参的转义(推荐使用)json =htmlFilter(json).trim();final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));return new ServletInputStream() {@Overridepublic boolean isFinished() {return true;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() {return bis.read();}};}private String htmlFilter(String s) {if (s == null) {return s;}final String line_flag = XssFilter.line_flag;// 换行特殊字符替换先,在AntiSamy 处理时,会将换行符处理成空格,所以在AntiSamy处理后将特殊字符替换成换行符;s = s.replace("\r\n", line_flag).replace("\r", line_flag).replace("\n", line_flag);s = HtmlFilterConfig.htmlFiler(s);s = s.replace(line_flag, "\n");return StringEscapeUtils.unescapeHtml4(s);}
}
public class HtmlFilterConfig {private static final Logger logger = LoggerFactory.getLogger(HtmlFilterConfig.class);private static HtmlFilter htmlFilter = null;public static String htmlFiler(String html) {if (htmlFilter == null) {return html;}return htmlFilter.htmlFiler(html);}/*** 初始化* @param htmlFilterClass* @param initParam*/public static void init(String htmlFilterClass, String initParam) {try {if (htmlFilterClass != null && htmlFilterClass.length() > 0) {htmlFilter = (HtmlFilter) Class.forName(htmlFilterClass).newInstance();}htmlFilter.init(initParam);} catch (InstantiationException e) {if (logger.isErrorEnabled()) {logger.error("HtmlFilter use user-defined filter:" + htmlFilterClass+ " instantiation error", e);}} catch (IllegalAccessException e) {if (logger.isErrorEnabled()) {logger.error("HtmlFilter use user-defined filter:" + htmlFilterClass+ " illegalAccess error", e);}} catch (ClassNotFoundException e) {if (logger.isErrorEnabled()) {logger.error("HtmlFilter use user-defined filter:" + htmlFilterClass+ " not found", e);}}}
}
Spring Boot版本:
AntiSamyConfig.java
@Configuration
public class AntiSamyConfig {/*** * 配置XSS过滤器** @return FilterRegistrationBean*/@Beanpublic FilterRegistrationBean<Filter> filterRegistrationBean() {FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(new XssFilter());filterRegistrationBean.addUrlPatterns("/*");filterRegistrationBean.setOrder(1);return filterRegistrationBean;}/*** 用于过滤Json类型数据的解析器** @param builder Jackson2ObjectMapperBuilder* @return ObjectMapper*/@Bean@Primarypublic ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {// 创建解析器ObjectMapper objectMapper = builder.createXmlMapper(false).build();// 注册解析器SimpleModule simpleModule = new SimpleModule("XssStringJsonSerializer");//入参和出参过滤选一个就好了,没必要两个都加//这里为了和XssHttpServletRequestWrapper统一,建议对入参进行处理//注册入参转义simpleModule.addDeserializer(String.class, new XssRequestWrapper.XssStringJsonDeserializer());//注册出参转义
// simpleModule.addSerializer(new XssRequestWrapper.XssStringJsonSerializer());objectMapper.registerModule(simpleModule);return objectMapper;}
}
XssFilter.java
public class XssFilter implements Filter {/*** 换行标识*/public static final String LINE_BREAK_FLAG = "[~line_brk_fg~]";@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// 拦截请求,处理XSS过滤chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);}@Overridepublic void destroy() {}
}
XssRequestWrapper.java
@Slf4j
public class XssRequestWrapper extends HttpServletRequestWrapper {private static Policy policy = null;// html过滤static {try {// 获取策略文件路径,策略文件需要放到项目的classpath下String antiSamyPath = Objects.requireNonNull(XssRequestWrapper.class.getClassLoader().getResource("antisamy-ebay.xml")).getFile();log.info("XssRequestWrapper::antiSamyPath 路径:{}", antiSamyPath);// 获取的文件路径中有空格时,空格会被替换为%20,在new一个File对象时会出现找不到路径的错误// 对路径进行解码以解决该问题antiSamyPath = URLDecoder.decode(antiSamyPath, "utf-8");log.info("XssRequestWrapper::antiSamyPath 路径:{}", antiSamyPath);// 指定策略文件policy = Policy.getInstance(antiSamyPath);} catch (UnsupportedEncodingException | PolicyException e) {log.error("XssRequestWrapper failure.", e);}}public XssRequestWrapper(HttpServletRequest request) {super(request);}/*** 过滤请求头** @param name 参数名* @return 参数值*/@Overridepublic String getHeader(String name) {String header = super.getHeader(name);// 如果Header为空,则直接返回,否则进行清洗return StringUtils.isBlank(header) ? header : xssClean(header);}@Overridepublic String getParameter(String name) {String parameter = super.getParameter(name);// 如果Parameter为空,则直接返回,否则进行清洗return StringUtils.isBlank(parameter) ? parameter : xssClean(parameter);}@Overridepublic Map<String, String[]> getParameterMap() {Map<String, String[]> requestMap = super.getParameterMap();requestMap.forEach((key, value) -> {for (int i = 0; i < value.length; i++) {log.info(value[i]);value[i] = xssClean(value[i]);log.info(value[i]);}});return requestMap;}@Overridepublic ServletInputStream getInputStream() throws IOException {// 非json类型,直接返回if (!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)&&!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {return super.getInputStream();}String json = IoUtil.read(super.getInputStream(), "utf-8");if (StrUtil.isEmpty(json)) {return super.getInputStream();}json = StringEscapeUtils.unescapeHtml4(json);// 这里要注意,json格式的参数不能直接使用hutool的EscapeUtil.escape, 因为它会把"也给转义,// 使得@RequestBody没办法解析成为一个正常的对象,所以我们自己实现一个过滤方法// 或者采用定制自己的objectMapper处理json出入参的转义(推荐使用)json =xssClean(json).trim();final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));return new ServletInputStream() {@Overridepublic boolean isFinished() {return true;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() {return bis.read();}};}@Overridepublic String[] getParameterValues(String name) {String[] parameterValues = super.getParameterValues(name);if (parameterValues != null) {int length = parameterValues.length;String[] newParameterValues = new String[length];for (int i = 0; i < length; i++) {// 清洗参数newParameterValues[i] = xssClean(parameterValues[i]);}return newParameterValues;}return super.getParameterValues(name);}/*** 使用AntiSamy清洗数据** @param value 需要清洗的数据* @return 清洗后的数据*/private String xssClean(String value) {try {final String LINE_BREAK_FLAG = XssFilter.LINE_BREAK_FLAG;// 换行特殊字符替换先,在AntiSamy 处理时,会将换行符处理成空格,所以在AntiSamy处理后将特殊字符替换成换行符;value = value.replace("\r\n", LINE_BREAK_FLAG).replace("\r", LINE_BREAK_FLAG).replace("\n", LINE_BREAK_FLAG);value = value.replace("/::<", "/::<");AntiSamy antiSamy = new AntiSamy();// 使用AntiSamy清洗数据final CleanResults cleanResults = antiSamy.scan(value, policy);// 获得安全的HTML输出value = cleanResults.getCleanHTML();// 替换 双引号""value = value.replaceAll("\"","'");value = value.replace("/::<", "/::<");value = value.replace(LINE_BREAK_FLAG, "\n");// 对转义的HTML特殊字符(<、>、"等)进行反转义,因为AntiSamy调用scan方法时会将特殊字符转义return StringEscapeUtils.unescapeHtml4(value);} catch (ScanException | PolicyException e) {e.printStackTrace();}return value;}/*** 通过修改Json序列化的方式来完成Json格式的XSS过滤*/public static class XssStringJsonSerializer extends JsonSerializer<String> {@Overridepublic Class<String> handledType() {return String.class;}@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {if (!StringUtils.isBlank(value)) {try {AntiSamy antiSamy = new AntiSamy();final CleanResults cleanResults = antiSamy.scan(value, XssRequestWrapper.policy);gen.writeString(StringEscapeUtils.unescapeHtml4(cleanResults.getCleanHTML()));} catch (PolicyException | ScanException e) {e.printStackTrace();}}}}/*** 处理json入参的转义*/public static class XssStringJsonDeserializer extends JsonDeserializer<String> {@Overridepublic Class<String> handledType() {return String.class;}//对入参转义@Overridepublic String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {String value = jsonParser.getValueAsString();if (value != null) {return EscapeUtil.escape(value.toString());}return value;}}
}
2、越权问题
原理:越权主要分为垂直越权和水平越权,垂直越权是指当一个普通用户使用管理员的信息能够获取到不属于自己权限内的信息。水平越权是指都是普通用户,但是不同部门不同组,却可以通过接口获取其他人的信息。
处理:我们此次主要采用的是引入Security 框架,通过@PreAuthorize注解和自定义权限认证方法去进行接口管控(此方法主要用于水平越权)。
相关代码:
登录信息放入Security:
UserDetails userDetails = new OperatorUserDetails(user, Arrays.asList(user.getPrivilege());UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
这两个注解很重要!!!
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overridepublic void configure(HttpSecurity httpSecurity) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();//url为白名单地址registry.antMatchers(url).permitAll();//允许跨域请求的OPTIONS请求registry.antMatchers(HttpMethod.OPTIONS).permitAll();registry.anyRequest().authenticated()// 自定义权限拒绝处理类.and().csrf().disable().exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler()).authenticationEntryPoint(restAuthenticationEntryPoint()).and().headers().frameOptions().disable().and().addFilterBefore(securityOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class);}/*** Override this method to configure {@link WebSecurity}. For example, if you wish to* ignore certain requests.** @param web*/@Overridepublic void configure(WebSecurity web) throws Exception {super.configure(web);web.httpFirewall(defaultHttpFireWall());}@Beanpublic RestfulAccessDeniedHandler restfulAccessDeniedHandler() {return new RestfulAccessDeniedHandler();}@Beanpublic RestAuthenticationEntryPoint restAuthenticationEntryPoint() {return new RestAuthenticationEntryPoint();}@Beanpublic SecurityOncePerRequestFilter securityOncePerRequestFilter() {return new SecurityOncePerRequestFilter();}@Beanpublic HttpFirewall defaultHttpFireWall() {return new DefaultHttpFirewall();}}
RestfulAccessDeniedHandler.java
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{@Overridepublic void handle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException e) throws IOException, ServletException {response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Cache-Control","no-cache");response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println("您没有权限"));response.getWriter().flush();}
}
RestAuthenticationEntryPoint.java
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Cache-Control","no-cache");response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println("您暂未登录");response.getWriter().flush();}
}
使用方法:在接口层次上加 @PreAuthorize(“@pms.hasPermission(‘自定义的权限值’)”)
/*** @Description 检查客服权限*/
@Component("pms")
public class PermissionService {/*** 检查权限* @param permissions* @return*/public boolean hasPermission(String ...permissions){Authentication authentication = SecurityContextHolder.getContext().getAuthentication();Object principal = authentication.getPrincipal();if ("anonymousUser".equals(principal)){return false;}OperatorUserDetails userDetails = (OperatorUserDetails) principal;User user = userDetails.getUser();for (String permission : permissions) {if (user.getPrivilege().hasPrivilege(permission)){return true;}}return false;}
}
SecurityWebApplicationInitializer.java 必须存在,不然会出现篡权问题
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
//public class SecurityWebApplicationInitializer {}
需要导入此配置。
@Import({ SecurityConfig.class})
注意:如果接口没有权限,默认是会返回500,所以为了进一步区分,所以建议拦截@ExceptionHandler(AccessDeniedException.class)和@ExceptionHandler(AuthenticationException.class)这两个异常。