Skip to content

Commit 78e8280

Browse files
committed
feat: Enforce signing and verifying of cookie value
Because a cookie is sent by an untrusted client we need to sign and verify it's content to be sure not to accept any garbage or tampered stuff. This reduces the risk of being vulnerable to deserializiation attacks because we only accept stuff we ourself signed before.
1 parent bd3563f commit 78e8280

File tree

4 files changed

+142
-8
lines changed

4 files changed

+142
-8
lines changed

src/main/java/com/innoq/spring/cookie/flash/CookieFlashMapManager.java

+47-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.innoq.spring.cookie.flash;
1717

18+
import com.innoq.spring.cookie.security.CookieValueSigner;
1819
import org.springframework.util.Assert;
1920
import org.springframework.web.servlet.FlashMap;
2021
import org.springframework.web.servlet.support.AbstractFlashMapManager;
@@ -24,23 +25,30 @@
2425
import javax.servlet.http.HttpServletResponse;
2526
import java.util.List;
2627

28+
import static java.nio.charset.StandardCharsets.UTF_8;
29+
import static java.security.MessageDigest.isEqual;
2730
import static org.springframework.web.util.WebUtils.getCookie;
2831

2932
public final class CookieFlashMapManager extends AbstractFlashMapManager {
3033

3134
private static final String DEFAULT_COOKIE_NAME = "flash";
3235

3336
private final FlashMapListCodec codec;
37+
private final CookieValueSigner signer;
3438
private final String cookieName;
3539

36-
public CookieFlashMapManager(FlashMapListCodec codec) {
37-
this(codec, DEFAULT_COOKIE_NAME);
40+
public CookieFlashMapManager(FlashMapListCodec codec,
41+
CookieValueSigner signer) {
42+
this(codec, signer, DEFAULT_COOKIE_NAME);
3843
}
3944

40-
public CookieFlashMapManager(FlashMapListCodec codec, String cookieName) {
45+
public CookieFlashMapManager(FlashMapListCodec codec,
46+
CookieValueSigner signer, String cookieName) {
4147
Assert.notNull(codec, "FlashMapListCodec must not be null");
48+
Assert.notNull(signer, "CookieValueSigner must not be null");
4249
Assert.hasText(cookieName, "Cookie name must not be null or empty");
4350
this.codec = codec;
51+
this.signer = signer;
4452
this.cookieName = cookieName;
4553
}
4654

@@ -52,7 +60,7 @@ protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
5260
}
5361

5462
final String value = cookie.getValue();
55-
return codec.decode(value);
63+
return decode(value);
5664
}
5765

5866
@Override
@@ -65,7 +73,7 @@ protected void updateFlashMaps(List<FlashMap> flashMaps,
6573
if (flashMaps.isEmpty()) {
6674
cookie.setMaxAge(0);
6775
} else {
68-
final String value = codec.encode(flashMaps);
76+
final String value = encode(flashMaps);
6977
cookie.setValue(value);
7078
}
7179
response.addCookie(cookie);
@@ -75,4 +83,38 @@ protected void updateFlashMaps(List<FlashMap> flashMaps,
7583
protected Object getFlashMapsMutex(HttpServletRequest request) {
7684
return null;
7785
}
86+
87+
private List<FlashMap> decode(String value) {
88+
final String[] signatureAndPayload = reverse(value).split("--", 2);
89+
if (signatureAndPayload.length != 2) {
90+
// TODO logging
91+
return null;
92+
}
93+
94+
final String signature = reverse(signatureAndPayload[0]);
95+
final String payload = reverse(signatureAndPayload[1]);
96+
97+
if (!isVerified(payload, signature)) {
98+
// TODO logging
99+
return null;
100+
}
101+
102+
return codec.decode(payload);
103+
}
104+
105+
106+
private String encode(List<FlashMap> flashMaps) {
107+
final String payload = codec.encode(flashMaps);
108+
final String signature = signer.sign(payload);
109+
return payload + "--" + signature;
110+
}
111+
112+
private boolean isVerified(String payload, String digest) {
113+
final String signature = signer.sign(payload);
114+
return isEqual(digest.getBytes(UTF_8), signature.getBytes(UTF_8));
115+
}
116+
117+
private static String reverse(String s) {
118+
return new StringBuilder(s).reverse().toString();
119+
}
78120
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright 2018 innoQ Deutschland GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.innoq.spring.cookie.security;
17+
18+
import static java.nio.charset.StandardCharsets.UTF_8;
19+
20+
public interface CookieValueSigner {
21+
22+
String sign(String payload);
23+
24+
static CookieValueSigner hmacSha1(String secret) {
25+
return new HmacCookieValueSigner("HmacSHA1", secret.getBytes(UTF_8));
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright 2018 innoQ Deutschland GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.innoq.spring.cookie.security;
17+
18+
import javax.crypto.Mac;
19+
import javax.crypto.spec.SecretKeySpec;
20+
import java.security.InvalidKeyException;
21+
import java.security.Key;
22+
import java.security.NoSuchAlgorithmException;
23+
24+
import static java.nio.charset.StandardCharsets.UTF_8;
25+
26+
final class HmacCookieValueSigner implements CookieValueSigner {
27+
28+
private final String algorithm;
29+
private final byte[] secret;
30+
31+
HmacCookieValueSigner(String algorithm, byte[] secret) {
32+
this.algorithm = algorithm;
33+
this.secret = secret;
34+
}
35+
36+
@Override
37+
public String sign(String payload) {
38+
try {
39+
final byte[] data = payload.getBytes(UTF_8);
40+
final Key key = new SecretKeySpec(secret, algorithm);
41+
42+
final Mac mac = Mac.getInstance(algorithm);
43+
mac.init(key);
44+
final byte[] result = mac.doFinal(data);
45+
46+
return bytesToHex(result);
47+
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
48+
// TODO: use own exception?
49+
throw new IllegalStateException("Unable to sign payload", e);
50+
}
51+
}
52+
53+
private static String bytesToHex(byte[] bytes) {
54+
final StringBuilder hex = new StringBuilder();
55+
for (byte b : bytes) {
56+
hex.append(byteToHex(b));
57+
}
58+
return hex.toString();
59+
}
60+
61+
private static String byteToHex(byte b) {
62+
return Integer.toString((b & 0xff) + 0x100, 16).substring(1);
63+
}
64+
}

src/test/java/com/innoq/spring/cookie/flash/CookieFlashMapManagerTest.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.innoq.spring.cookie.flash;
1717

1818
import com.innoq.spring.cookie.flash.codec.jackson.JacksonFlashMapListCodec;
19+
import com.innoq.spring.cookie.security.CookieValueSigner;
1920
import org.junit.jupiter.api.Test;
2021
import org.springframework.mock.web.MockHttpServletRequest;
2122
import org.springframework.mock.web.MockHttpServletResponse;
@@ -33,7 +34,7 @@
3334
class CookieFlashMapManagerTest {
3435

3536
CookieFlashMapManager sut = new CookieFlashMapManager(
36-
JacksonFlashMapListCodec.create());
37+
JacksonFlashMapListCodec.create(), CookieValueSigner.hmacSha1("abc"));
3738

3839
@Test
3940
void retrieveFlashMaps_withNoCookiePresent_returnsNull() {
@@ -46,7 +47,7 @@ void retrieveFlashMaps_withNoCookiePresent_returnsNull() {
4647

4748
@Test
4849
void retrieveFlashMaps_withValidCookie_returnsFlashMaps() {
49-
String cookieValue = "W3siYXR0cmlidXRlcyI6eyJmb28iOm51bGwsImJhciI6NDcxMSwiYmF6IjoibG9yZW0gaXBzdW0ifSwiZXhwaXJhdGlvblRpbWUiOjQ3MTEsInRhcmdldFJlcXVlc3RQYXJhbXMiOnsiZm9vIjpbXSwiYmFyIjpbImZvbyJdLCJiYXoiOlsibG9yZW0iLCJpcHN1bSJdfSwidGFyZ2V0UmVxdWVzdFBhdGgiOiIvZm9vIn1dCg==";
50+
String cookieValue = "W3siYXR0cmlidXRlcyI6eyJmb28iOm51bGwsImJhciI6NDcxMSwiYmF6IjoibG9yZW0gaXBzdW0ifSwiZXhwaXJhdGlvblRpbWUiOjQ3MTEsInRhcmdldFJlcXVlc3RQYXJhbXMiOnsiZm9vIjpbXSwiYmFyIjpbImZvbyJdLCJiYXoiOlsibG9yZW0iLCJpcHN1bSJdfSwidGFyZ2V0UmVxdWVzdFBhdGgiOiIvZm9vIn1dCg==--aa17ee8faf0bbe77a0949de6d5c593bd1e39718c";
5051

5152
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
5253
request.setCookies(new Cookie("flash", cookieValue));
@@ -82,7 +83,7 @@ void updateFlashMaps_withSingleFlashMap_writesCookie() {
8283
.hasFieldOrPropertyWithValue("httpOnly", true);
8384

8485
String cookieValue = response.getCookie("flash").getValue();
85-
assertThat(cookieValue).isEqualTo("W3siYXR0cmlidXRlcyI6e30sImV4cGlyYXRpb25UaW1lIjotMSwidGFyZ2V0UmVxdWVzdFBhcmFtcyI6e30sInRhcmdldFJlcXVlc3RQYXRoIjpudWxsfV0=");
86+
assertThat(cookieValue).isEqualTo("W3siYXR0cmlidXRlcyI6e30sImV4cGlyYXRpb25UaW1lIjotMSwidGFyZ2V0UmVxdWVzdFBhcmFtcyI6e30sInRhcmdldFJlcXVlc3RQYXRoIjpudWxsfV0=--daa79b20816b076ceb9f628bef9a82792fe9b5fa");
8687
}
8788

8889
@Test

0 commit comments

Comments
 (0)