Skip to content

Commit ae4b0d9

Browse files
committed
Improve timestamp deserialization and SSRF protection
Enhanced TurAemUnixTimestamp to handle both seconds and milliseconds by padding and checking length. Improved SSRF mitigation in TurIntegrationAPI by refining host/scheme checks and proxy path validation, now requiring paths to start with '/api/' and using normalized paths to prevent traversal.
1 parent 0928181 commit ae4b0d9

File tree

2 files changed

+56
-48
lines changed

2 files changed

+56
-48
lines changed

turing-aem-commons/src/main/java/com/viglet/turing/connector/aem/commons/deserializer/TurAemUnixTimestamp.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,24 @@ public TurAemUnixTimestamp(Class<?> vc) {
3838
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
3939
throws IOException {
4040
String timestamp = jsonParser.getText().trim();
41-
if (timestamp == null || timestamp.trim().isEmpty()) {
41+
if (timestamp.isEmpty()) {
4242
return null;
4343
}
4444
try {
45-
return new Date(Long.parseLong(timestamp) * 1000);
45+
// Pad to at least 10 digits (seconds), but not arbitrarily to 12
46+
while (timestamp.length() < 10) {
47+
timestamp += "0";
48+
}
49+
long timeMillis;
50+
if (timestamp.length() == 10) {
51+
// Assume seconds, convert to milliseconds
52+
timeMillis = Long.parseLong(timestamp) * 1000L;
53+
} else {
54+
// Assume milliseconds
55+
timeMillis = Long.parseLong(timestamp);
56+
}
57+
Date date = new Date(timeMillis);
58+
return date;
4659
} catch (NumberFormatException e) {
4760
log.error("Unable to deserialize timestamp: {}", timestamp, e);
4861
return null;

turing-app/src/main/java/com/viglet/turing/api/integration/TurIntegrationAPI.java

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,35 @@
11
/*
22
* Copyright (C) 2016-2024 the original author or authors.
33
*
4-
* Licensed to the Apache Software Foundation (ASF) under one
5-
* or more contributor license agreements. See the NOTICE file
6-
* distributed with this work for additional information
7-
* regarding copyright ownership. The ASF licenses this file
8-
* to you under the Apache License, Version 2.0 (the
9-
* "License"); you may not use this file except in compliance
10-
* with the License. You may obtain a copy of the License at
4+
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
5+
* agreements. See the NOTICE file distributed with this work for additional information regarding
6+
* copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License. You may obtain a
8+
* copy of the License at
119
*
12-
* http://www.apache.org/licenses/LICENSE-2.0
10+
* http://www.apache.org/licenses/LICENSE-2.0
1311
*
14-
* Unless required by applicable law or agreed to in writing,
15-
* software distributed under the License is distributed on an
16-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17-
* KIND, either express or implied. See the License for the
18-
* specific language governing permissions and limitations
19-
* under the License.
12+
* Unless required by applicable law or agreed to in writing, software distributed under the License
13+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14+
* or implied. See the License for the specific language governing permissions and limitations under
15+
* the License.
2016
*/
2117
package com.viglet.turing.api.integration;
2218

2319
import java.io.OutputStream;
2420
import java.net.HttpURLConnection;
2521
import java.net.URI;
26-
22+
import java.nio.file.Paths;
2723
import org.apache.http.HttpHeaders;
2824
import org.springframework.http.MediaType;
29-
import java.nio.file.Paths;
3025
import org.springframework.web.bind.annotation.PathVariable;
3126
import org.springframework.web.bind.annotation.RequestMapping;
3227
import org.springframework.web.bind.annotation.RequestMethod;
3328
import org.springframework.web.bind.annotation.RestController;
34-
3529
import com.google.common.io.ByteStreams;
3630
import com.google.common.io.CharStreams;
3731
import com.viglet.turing.persistence.model.integration.TurIntegrationInstance;
3832
import com.viglet.turing.persistence.repository.integration.TurIntegrationInstanceRepository;
39-
4033
import io.swagger.v3.oas.annotations.tags.Tag;
4134
import jakarta.servlet.http.HttpServletRequest;
4235
import jakarta.servlet.http.HttpServletResponse;
@@ -55,43 +48,45 @@ public class TurIntegrationAPI {
5548
this.turIntegrationInstanceRepository = turIntegrationInstanceRepository;
5649
}
5750

58-
@RequestMapping(value = "**", method = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT,
59-
RequestMethod.DELETE }, produces = { MediaType.APPLICATION_JSON_VALUE })
51+
@RequestMapping(value = "**", method = {RequestMethod.GET, RequestMethod.POST,
52+
RequestMethod.PUT, RequestMethod.DELETE}, produces = {MediaType.APPLICATION_JSON_VALUE})
6053
public void indexAnyRequest(HttpServletRequest request, HttpServletResponse response,
6154
@PathVariable String integrationId) {
62-
turIntegrationInstanceRepository.findById(integrationId)
63-
.ifPresent(turIntegrationInstance -> proxy(turIntegrationInstance, request, response));
55+
turIntegrationInstanceRepository.findById(integrationId).ifPresent(
56+
turIntegrationInstance -> proxy(turIntegrationInstance, request, response));
6457
}
6558

6659
public void proxy(TurIntegrationInstance turIntegrationInstance, HttpServletRequest request,
6760
HttpServletResponse response) {
6861
try {
69-
String endpoint = turIntegrationInstance.getEndpoint() +
70-
request.getRequestURI()
71-
.replace("/api/v2/integration/" + turIntegrationInstance.getId(), "/api/v2");
62+
String endpoint = turIntegrationInstance.getEndpoint() + request.getRequestURI()
63+
.replace("/api/v2/integration/" + turIntegrationInstance.getId(), "/api/v2");
7264
log.debug("Executing: {}", endpoint);
7365
URI baseUri = URI.create(turIntegrationInstance.getEndpoint());
7466
URI fullUri = URI.create(endpoint);
75-
// SSRF Mitigation: Only allow requests to the same host and scheme as the registered endpoint
76-
if (!baseUri.getHost().equalsIgnoreCase(fullUri.getHost()) ||
77-
!baseUri.getScheme().equalsIgnoreCase(fullUri.getScheme())) {
78-
log.warn("Blocked SSRF attempt: attempted host={}, scheme={}", fullUri.getHost(), fullUri.getScheme());
67+
// SSRF Mitigation: Only allow requests to the same host and scheme as the registered
68+
// endpoint
69+
if (!baseUri.getHost().equalsIgnoreCase(fullUri.getHost())
70+
|| !baseUri.getScheme().equalsIgnoreCase(fullUri.getScheme())) {
71+
log.warn("Blocked SSRF attempt: attempted host={}, scheme={}", fullUri.getHost(),
72+
fullUri.getScheme());
7973
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
8074
response.getWriter().write("{\"error\": \"Forbidden proxy target\"}");
8175
return;
8276
}
8377
// Validate that the path is safe and does not contain traversal or forbidden segments
8478
if (!isValidProxyPath(fullUri.getPath())) {
85-
log.warn("Blocked SSRF attempt: invalid or unauthorized path: {}", fullUri.getPath());
79+
log.warn("Blocked SSRF attempt: invalid or unauthorized path: {}",
80+
fullUri.getPath());
8681
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
8782
response.getWriter().write("{\"error\": \"Forbidden proxy path\"}");
8883
return;
8984
}
90-
HttpURLConnection connectorEnpoint = (HttpURLConnection) fullUri
91-
.toURL().openConnection();
85+
HttpURLConnection connectorEnpoint =
86+
(HttpURLConnection) fullUri.toURL().openConnection();
9287
connectorEnpoint.setRequestMethod(request.getMethod());
93-
request.getHeaderNames().asIterator().forEachRemaining(
94-
headerName -> connectorEnpoint.setRequestProperty(headerName, request.getHeader(headerName)));
88+
request.getHeaderNames().asIterator().forEachRemaining(headerName -> connectorEnpoint
89+
.setRequestProperty(headerName, request.getHeader(headerName)));
9590
String method = request.getMethod();
9691
if (method.equals(PUT) || method.equals(POST)) {
9792
connectorEnpoint.setDoOutput(true);
@@ -102,29 +97,29 @@ public void proxy(TurIntegrationInstance turIntegrationInstance, HttpServletRequ
10297
}
10398
response.setStatus(connectorEnpoint.getResponseCode());
10499
ByteStreams.copy(connectorEnpoint.getInputStream(), response.getOutputStream());
105-
connectorEnpoint.getHeaderFields()
106-
.forEach((header, values) -> values.forEach(value -> {
107-
if (header != null && !header.equals(HttpHeaders.TRANSFER_ENCODING)) {
108-
log.debug("Header: {} = {}", header, value);
109-
response.setHeader(header, value);
110-
}
111-
}));
100+
connectorEnpoint.getHeaderFields().forEach((header, values) -> values.forEach(value -> {
101+
if (header != null && !header.equals(HttpHeaders.TRANSFER_ENCODING)) {
102+
log.debug("Header: {} = {}", header, value);
103+
response.setHeader(header, value);
104+
}
105+
}));
112106
} catch (Exception e) {
113107
log.error(e.getMessage(), e);
114108
}
115109
}
116110

117111
/**
118-
* Validates that the proxy path is safe: normalized, does not contain directory traversal,
119-
* and starts with the expected API prefix.
112+
* Validates that the proxy path is safe: normalized, does not contain directory traversal, and
113+
* starts with the expected API prefix.
120114
*/
121115
private boolean isValidProxyPath(String path) {
122-
if (path == null) return false;
123-
String normalized = java.nio.file.Paths.get(path).normalize().toString();
116+
if (path == null)
117+
return false;
124118
// Must start with allowed prefix after normalization (prevent access to internal endpoints)
125-
if (!normalized.startsWith("/api/v2/")) {
119+
if (!path.startsWith("/api/")) {
126120
return false;
127121
}
122+
String normalized = Paths.get(path).normalize().toString();
128123
// Disallow any attempts at directory traversal
129124
if (normalized.contains("..")) {
130125
return false;

0 commit comments

Comments
 (0)