Skip to content

Commit a0289f8

Browse files
committed
Bug #14176 Fix vuln by introducing injection checking in entity
emboddied within incoming HTTP requests. The checking is performed only if the request emboddies a web entity encoded in a supported content type, id est in JSON or in XML.
1 parent 14b5b9d commit a0289f8

File tree

3 files changed

+207
-31
lines changed

3 files changed

+207
-31
lines changed

core-rs/src/main/java/org/silverpeas/core/web/http/HttpRequest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public class HttpRequest extends HttpServletRequestWrapper {
6969

7070
private List<FileItem> fileItems = null;
7171

72-
private HttpRequest(HttpServletRequest request) {
72+
protected HttpRequest(HttpServletRequest request) {
7373
super(request);
7474
// The decorated request is put into attributes in order to provide it to the REST web
7575
// services that deals with proxies...

core-web/src/main/java/org/silverpeas/core/web/filter/MassiveWebSecurityFilter.java

+204-28
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,32 @@
2626
import org.apache.commons.lang3.time.DurationFormatUtils;
2727
import org.silverpeas.core.admin.user.model.User;
2828
import org.silverpeas.core.cache.service.CacheAccessorProvider;
29+
import org.silverpeas.core.jcr.webdav.WebDavProtocol;
2930
import org.silverpeas.core.persistence.jdbc.DBUtil;
30-
import org.silverpeas.kernel.util.StringUtil;
3131
import org.silverpeas.core.util.URLUtil;
32-
import org.silverpeas.kernel.logging.SilverLogger;
3332
import org.silverpeas.core.util.security.SecuritySettings;
3433
import org.silverpeas.core.web.SilverpeasWebResource;
3534
import org.silverpeas.core.web.filter.exception.WebSecurityException;
3635
import org.silverpeas.core.web.filter.exception.WebSqlInjectionSecurityException;
3736
import org.silverpeas.core.web.filter.exception.WebXssInjectionSecurityException;
3837
import org.silverpeas.core.web.http.HttpRequest;
39-
import org.silverpeas.core.jcr.webdav.WebDavProtocol;
38+
import org.silverpeas.kernel.annotation.NonNull;
39+
import org.silverpeas.kernel.logging.SilverLogger;
40+
import org.silverpeas.kernel.util.StringUtil;
4041

41-
import javax.servlet.Filter;
42-
import javax.servlet.FilterChain;
43-
import javax.servlet.FilterConfig;
44-
import javax.servlet.ServletException;
45-
import javax.servlet.ServletRequest;
46-
import javax.servlet.ServletResponse;
42+
import javax.servlet.*;
4743
import javax.servlet.http.HttpServletRequest;
4844
import javax.servlet.http.HttpServletResponse;
45+
import javax.ws.rs.InternalServerErrorException;
4946
import javax.ws.rs.core.UriBuilder;
47+
import java.io.BufferedInputStream;
5048
import java.io.IOException;
49+
import java.io.InputStream;
50+
import java.io.OutputStream;
5151
import java.util.ArrayList;
5252
import java.util.List;
5353
import java.util.Map;
54+
import java.util.Optional;
5455
import java.util.regex.Matcher;
5556
import java.util.regex.Pattern;
5657

@@ -62,6 +63,7 @@
6263
* <p>
6364
* For now, this filter ensures HTTPS is used in secured connections, blocks content sniffing of web
6465
* browsers, and checks XSS and SQL injections in URLs.
66+
*
6567
* @author Yohann Chastagnier
6668
*/
6769
public class MassiveWebSecurityFilter implements Filter {
@@ -128,7 +130,7 @@ public class MassiveWebSecurityFilter implements Filter {
128130
@Override
129131
public void doFilter(final ServletRequest request, final ServletResponse response,
130132
final FilterChain chain) throws IOException, ServletException {
131-
final HttpRequest httpRequest = (HttpRequest) request;
133+
final HttpRequest httpRequest = new HttpRequestWrapper((HttpRequest) request);
132134
final HttpServletResponse httpResponse = (HttpServletResponse) response;
133135
try {
134136
setDefaultSecurity(httpRequest, httpResponse);
@@ -205,11 +207,40 @@ private void checkWebInjection(final HttpRequest httpRequest,
205207
// this header isn't taken in charge by all web browsers.
206208
httpResponse.setHeader("X-XSS-Protection", "1");
207209
}
210+
checkRequestEntityForInjection(httpRequest);
208211
checkRequestParametersForInjection(httpRequest, isWebSqlInjectionSecurityEnabled,
209212
isWebXssInjectionSecurityEnabled);
210213
}
211214
}
212215

216+
private void checkRequestEntityForInjection(final HttpRequest request)
217+
throws WebSqlInjectionSecurityException, WebXssInjectionSecurityException {
218+
long start = System.currentTimeMillis();
219+
try {
220+
boolean hasSupportedWebEntity = Optional.ofNullable(request.getContentType())
221+
.map(String::toLowerCase)
222+
.filter(c -> c.contains("json") || c.contains("xml"))
223+
.isPresent();
224+
if (hasSupportedWebEntity) {
225+
String charset = request.getCharacterEncoding() == null ? "UTF-8" :
226+
request.getCharacterEncoding();
227+
InputStream body = request.getInputStream();
228+
if (body.markSupported()) {
229+
body.mark(Integer.MAX_VALUE);
230+
String entity = new String(body.readAllBytes(), charset);
231+
checkValueForInjection(entity, true, true);
232+
body.reset();
233+
}
234+
}
235+
} catch (IOException e) {
236+
throw new InternalServerErrorException(e);
237+
} finally {
238+
long end = System.currentTimeMillis();
239+
logger.debug("Massive Web Security Verify on request entity: " +
240+
DurationFormatUtils.formatDurationHMS(end - start));
241+
}
242+
}
243+
213244
private void checkRequestParametersForInjection(final HttpRequest httpRequest,
214245
final boolean isWebSqlInjectionSecurityEnabled,
215246
final boolean isWebXssInjectionSecurityEnabled)
@@ -231,44 +262,50 @@ private void checkRequestParametersForInjection(final HttpRequest httpRequest,
231262
}
232263
} finally {
233264
long end = System.currentTimeMillis();
234-
logger.debug("Massive Web Security Verify duration : " +
265+
logger.debug("Massive Web Security Verify on request parameters: " +
235266
DurationFormatUtils.formatDurationHMS(end - start));
236267
}
237268
}
238269

239270
private void checkParameterValues(final Map.Entry<String, String[]> parameterEntry,
240271
final boolean sqlInjectionToVerify, final boolean xssInjectionToVerify)
241272
throws WebSqlInjectionSecurityException, WebXssInjectionSecurityException {
242-
Matcher patternMatcherFound;
243273
for (String parameterValue : parameterEntry.getValue()) {
274+
checkValueForInjection(parameterValue, sqlInjectionToVerify, xssInjectionToVerify);
275+
}
276+
}
244277

245-
// Each sequence of spaces is replaced by one space
246-
parameterValue = parameterValue.replaceAll("\\s+", " ");
247-
248-
// SQL injections?
249-
if (sqlInjectionToVerify && (patternMatcherFound =
250-
findPatternMatcherFromString(SQL_PATTERNS, parameterValue, true)) != null) {
278+
private void checkValueForInjection(String value, boolean sqlInjectionToVerify,
279+
boolean xssInjectionToVerify) throws WebSqlInjectionSecurityException,
280+
WebXssInjectionSecurityException {
281+
Matcher patternMatcherFound;
282+
// Each sequence of spaces is replaced by one space
283+
value = value.replaceAll("\\s+", " ");
251284

252-
if (!verifySqlDeeply(patternMatcherFound, parameterValue)) {
253-
patternMatcherFound = null;
254-
}
285+
// SQL injections?
286+
if (sqlInjectionToVerify && (patternMatcherFound =
287+
findPatternMatcherFromString(SQL_PATTERNS, value, true)) != null) {
255288

256-
if (patternMatcherFound != null) {
257-
throw new WebSqlInjectionSecurityException();
258-
}
289+
if (!verifySqlDeeply(patternMatcherFound, value)) {
290+
patternMatcherFound = null;
259291
}
260292

261-
// XSS injections?
262-
if (xssInjectionToVerify &&
263-
findPatternMatcherFromString(XSS_PATTERNS, parameterValue, false) != null) {
264-
throw new WebXssInjectionSecurityException();
293+
if (patternMatcherFound != null) {
294+
throw new WebSqlInjectionSecurityException();
265295
}
266296
}
297+
298+
// XSS injections?
299+
if (xssInjectionToVerify &&
300+
findPatternMatcherFromString(XSS_PATTERNS, value, false) != null) {
301+
throw new WebXssInjectionSecurityException();
302+
}
267303
}
268304

269305
/**
270306
* Verifies deeply a matched SQL string. Indeed, throwing an exception of XSS attack only on SQL
271307
* detection is not enough. This method tries to detect a known table name from the SQL string.
308+
*
272309
* @param matcherFound a pattern matcher
273310
* @param statement a SQL statement to check
274311
* @return true of the SQL statement is considered as safe. False otherwise.
@@ -297,6 +334,7 @@ private boolean verifySqlDeeply(final Matcher matcherFound, String statement) {
297334
/**
298335
* Extracts the whole table name matching the given pattern. Indeed, the matcher can find a table
299336
* name that is a part of another one.
337+
*
300338
* @param matcher a pattern matcher.
301339
* @param matchedString a SQL statement part
302340
* @return a whole table name
@@ -330,6 +368,7 @@ private String extractTableNameWholeWord(Matcher matcher, String matchedString)
330368
/**
331369
* Gets a pattern that permits to check deeply a detected SELECT FROM with known table names. A
332370
* cache is handled by this method in order to avoid building at every call the same pattern.
371+
*
333372
* @return a regexp pattern.
334373
*/
335374
private synchronized Pattern getSqlTableNamesPattern() {
@@ -357,6 +396,7 @@ private synchronized Pattern getSqlTableNamesPattern() {
357396

358397
/**
359398
* Must the given parameter be skipped from SQL injection verifying?
399+
*
360400
* @param parameterName name of a parameter.
361401
* @return true if the given parameter has to be skipped. False otherwise.
362402
*/
@@ -367,6 +407,7 @@ private boolean mustTheParameterBeVerifiedForSqlVerifications(String parameterNa
367407

368408
/**
369409
* Must the given parameter be skipped from XSS injection verifying?
410+
*
370411
* @param parameterName name of a parameter.
371412
* @return true of the given parameter has to be skipped. False otherwise.
372413
*/
@@ -378,6 +419,7 @@ private boolean mustTheParameterBeVerifiedForXssVerifications(String parameterNa
378419
/**
379420
* Gets the matcher corresponding to the pattern in the given list of patterns and for which the
380421
* specified string is compliant.
422+
*
381423
* @param patterns a list of pattern to apply on the given string.
382424
* @param string a string to check.
383425
* @param startsAndEndsByWholeWord a flag indicating the pattern should match for the first and
@@ -401,6 +443,7 @@ private Matcher findPatternMatcherFromString(List<Pattern> patterns, String stri
401443

402444
/**
403445
* Verifies that the first word of matching starts with a whole word.
446+
*
404447
* @param matcher a matcher.
405448
* @param matchedString a string.
406449
* @return true if the first word of matching starts with a whole word
@@ -412,6 +455,7 @@ private boolean verifyMatcherStartingByAWord(Matcher matcher, String matchedStri
412455

413456
/**
414457
* Verifies that the first word of matching ends with a whole word.
458+
*
415459
* @param matcher a matcher
416460
* @param matchedString a string
417461
* @return true if the first word of matching ends with a whole word.
@@ -435,4 +479,136 @@ public void init(final FilterConfig filterConfig) throws ServletException {
435479
public void destroy() {
436480
// Nothing to do.
437481
}
482+
483+
/**
484+
* Wrapper of an {@link HttpRequest} to buffer the input stream on its body in order to
485+
* allow access and back-and-forth navigation within the body content through the input
486+
* stream.
487+
*/
488+
private static class HttpRequestWrapper extends HttpRequest {
489+
490+
private BufferedServletInputStream input;
491+
492+
/**
493+
* Constructs a request object wrapping the given request.
494+
*
495+
* @param request the {@link HttpServletRequest} to be wrapped.
496+
* @throws IllegalArgumentException if the request is null
497+
*/
498+
public HttpRequestWrapper(HttpRequest request) {
499+
super(request);
500+
}
501+
502+
/**
503+
* Gets the input stream on the content of the request's body. The input stream is buffered and,
504+
* as such, position in the stream can be marked and hence reset to the last mark (last
505+
* marked position in the stream).
506+
* @return a buffered {@link ServletInputStream}.
507+
* @throws IOException if an error occurs while opening an input stream on the content of the
508+
* request's body.
509+
*/
510+
@Override
511+
public ServletInputStream getInputStream() throws IOException {
512+
if (input == null) {
513+
input = new BufferedServletInputStream(super.getInputStream());
514+
}
515+
return input;
516+
}
517+
518+
private static class BufferedServletInputStream extends ServletInputStream {
519+
520+
private final ServletInputStream inputStream;
521+
private final BufferedInputStream buffer;
522+
523+
private BufferedServletInputStream(ServletInputStream inputStream) {
524+
this.inputStream = inputStream;
525+
this.buffer = new BufferedInputStream(inputStream);
526+
}
527+
528+
@Override
529+
public boolean isFinished() {
530+
try {
531+
return this.buffer.available() == 0;
532+
} catch (IOException e) {
533+
return true;
534+
}
535+
}
536+
537+
@Override
538+
public boolean isReady() {
539+
return !isFinished();
540+
}
541+
542+
@Override
543+
public void setReadListener(ReadListener readListener) {
544+
this.inputStream.setReadListener(readListener);
545+
}
546+
547+
@Override
548+
public int read() throws IOException {
549+
return buffer.read();
550+
}
551+
552+
@Override
553+
public int read(@NonNull byte[] b, int off, int len) throws IOException {
554+
return buffer.read(b, off, len);
555+
}
556+
557+
@Override
558+
public long skip(long n) throws IOException {
559+
return buffer.skip(n);
560+
}
561+
562+
@Override
563+
public int available() throws IOException {
564+
return buffer.available();
565+
}
566+
567+
@Override
568+
public synchronized void mark(int readLimit) {
569+
buffer.mark(readLimit);
570+
}
571+
572+
@Override
573+
public synchronized void reset() throws IOException {
574+
buffer.reset();
575+
}
576+
577+
@Override
578+
public boolean markSupported() {
579+
return buffer.markSupported();
580+
}
581+
582+
@Override
583+
public void close() throws IOException {
584+
buffer.close();
585+
}
586+
587+
@Override
588+
public int read(@NonNull byte[] b) throws IOException {
589+
return buffer.read(b);
590+
}
591+
592+
@Override
593+
public byte[] readAllBytes() throws IOException {
594+
return buffer.readAllBytes();
595+
}
596+
597+
@Override
598+
public byte[] readNBytes(int len) throws IOException {
599+
return buffer.readNBytes(len);
600+
}
601+
602+
@Override
603+
public int readNBytes(byte[] b, int off, int len) throws IOException {
604+
return buffer.readNBytes(b, off, len);
605+
}
606+
607+
@Override
608+
public long transferTo(OutputStream out) throws IOException {
609+
return buffer.transferTo(out);
610+
}
611+
612+
}
613+
}
438614
}

core-web/src/main/java/org/silverpeas/core/webapi/calendar/CalendarWebManager.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ public List<CalendarEventOccurrence> getEventOccurrencesOf(LocalDate startDate,
675675
* @param startDate the start date of time window.
676676
* @param endDate the end date of time window.
677677
* @param users the users to filter on.
678-
* @return a list of entities of calendar event occurrences mapped by user identifiers.
678+
* @return a map of a list of entities of calendar event occurrences mapped by user identifiers.
679679
*/
680680
protected Map<String, List<CalendarEventOccurrence>> getAllEventOccurrencesByUserIds(
681681
final Pair<List<String>, User> currentUserAndComponentInstanceId, LocalDate startDate,
@@ -767,7 +767,7 @@ public Stream<CalendarEventOccurrence> getNextEventOccurrences(final List<String
767767

768768
/**
769769
* Gets next event time windows from settings.
770-
* @return list of integer which represents months.
770+
* @return an array of integers representing months.
771771
*/
772772
protected Integer[] getNextEventTimeWindows() {
773773
final String[] timeWindows = settings.getString("calendar.nextEvents.time.windows").split(",");

0 commit comments

Comments
 (0)