原理:使用一些script脚本和html标签注入进系统,然后进行侵入。
例如:
处理方式:
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.2antisamy.version>
<dependency>
<groupId>org.owasp.antisamygroupId>
<artifactId>antisamyartifactId>
<version>${antisamy.version}version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
exclusion>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simpleartifactId>
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~]";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 拦截请求,处理XSS过滤
chain.doFilter(new XSSHttpServletRequestWrapper((HttpServletRequest) request), response);
}
@Override
public 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
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return htmlFilter(value);
}
/**
* 方法说明:过滤掉字符
*
* @param name
* @return
*/
@Override
public String getHeader(String name) {
return htmlFilter(super.getHeader(name));
}
/**
* 方法说明:过滤掉字符
*
* @param name
* @return
*/
@Override
public 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;
}
@Override
public 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() {
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public 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
*/
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(new XssFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
/**
* 用于过滤Json类型数据的解析器
*
* @param builder Jackson2ObjectMapperBuilder
* @return ObjectMapper
*/
@Bean
@Primary
public 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~]";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 拦截请求,处理XSS过滤
chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
}
@Override
public 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 参数值
*/
@Override
public String getHeader(String name) {
String header = super.getHeader(name);
// 如果Header为空,则直接返回,否则进行清洗
return StringUtils.isBlank(header) ? header : xssClean(header);
}
@Override
public String getParameter(String name) {
String parameter = super.getParameter(name);
// 如果Parameter为空,则直接返回,否则进行清洗
return StringUtils.isBlank(parameter) ? parameter : xssClean(parameter);
}
@Override
public 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;
}
@Override
public 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() {
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return bis.read();
}
};
}
@Override
public 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> {
@Override
public Class<String> handledType() {
return String.class;
}
@Override
public 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> {
@Override
public Class<String> handledType() {
return String.class;
}
//对入参转义
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String value = jsonParser.getValueAsString();
if (value != null) {
return EscapeUtil.escape(value.toString());
}
return value;
}
}
}
原理:越权主要分为垂直越权和水平越权,垂直越权是指当一个普通用户使用管理员的信息能够获取到不属于自己权限内的信息。水平越权是指都是普通用户,但是不同部门不同组,却可以通过接口获取其他人的信息。
处理:我们此次主要采用的是引入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 {
@Override
public 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
*/
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.httpFirewall(defaultHttpFireWall());
}
@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
return new RestfulAccessDeniedHandler();
}
@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
@Bean
public SecurityOncePerRequestFilter securityOncePerRequestFilter() {
return new SecurityOncePerRequestFilter();
}
@Bean
public HttpFirewall defaultHttpFireWall() {
return new DefaultHttpFirewall();
}
}
RestfulAccessDeniedHandler.java
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public 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 {
@Override
public 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)这两个异常。