diff --git a/apps/testing/src/main/java/com/akto/test_editor/Utils.java b/apps/testing/src/main/java/com/akto/test_editor/Utils.java index 56b82f9795..bb83f70c10 100644 --- a/apps/testing/src/main/java/com/akto/test_editor/Utils.java +++ b/apps/testing/src/main/java/com/akto/test_editor/Utils.java @@ -13,9 +13,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.bouncycastle.jce.provider.JDKDSASigner.stdDSA; + import com.akto.dto.OriginalHttpRequest; import com.akto.dto.RawApi; +import com.akto.dto.test_editor.ExecutorSingleOperationResp; import com.akto.dto.testing.UrlModifierPayload; +import com.akto.util.Constants; import com.akto.util.JSONUtils; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; @@ -27,6 +31,8 @@ import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; +import okhttp3.*; + public class Utils { private static final ObjectMapper mapper = new ObjectMapper(); @@ -617,4 +623,70 @@ public static String convertToHarPayload(String message, int akto_account_id, in return mapper.writeValueAsString(result); } + public static String extractValue(String keyValue, String key) { + String result = ""; + if (keyValue.contains(key)) { + result = keyValue.split(key)[1].split("[,}]")[0]; + result = result.replaceAll("\\}$", ""); + result = result.trim(); + } + return result; + } + + public static ExecutorSingleOperationResp sendRequestToSsrfServer(String requestUrl, String redirectUrl, String tokenVal){ + RequestBody emptyBody = RequestBody.create(new byte[]{}, null); + + Request request = new Request.Builder() + .url(requestUrl) + .addHeader("x-akto-redirect-url", redirectUrl) + .addHeader(Constants.AKTO_TOKEN_KEY, tokenVal) + .post(emptyBody) + .build(); + + OkHttpClient client = new OkHttpClient(); + Response okResponse = null; + + try { + okResponse = client.newCall(request).execute(); + if (!okResponse.isSuccessful()) { + return new ExecutorSingleOperationResp(false,"Could not send request to the ssrf server."); + } + return new ExecutorSingleOperationResp(true, ""); + }catch (Exception e){ + return new ExecutorSingleOperationResp(false, e.getMessage()); + } + } + + public static Boolean sendRequestToSsrfServer(String url){ + String requestUrl = ""; + if(!(url.startsWith("http"))){ + String hostName ="https://test-services.akto.io/"; + if(System.getenv("SSRF_SERVICE_NAME") != null && System.getenv("SSRF_SERVICE_NAME").length() > 0){ + hostName = System.getenv("SSRF_SERVICE_NAME"); + } + requestUrl = hostName + url; + } + + Request request = new Request.Builder() + .url(requestUrl) + .get() + .build(); + + OkHttpClient client = new OkHttpClient(); + Response okResponse = null; + + try { + okResponse = client.newCall(request).execute(); + if (!okResponse.isSuccessful()) { + return false; + }else{ + ResponseBody responseBody = okResponse.body(); + BasicDBObject bd = BasicDBObject.parse(responseBody.string()); + return bd.getBoolean("url-hit"); + } + }catch (Exception e){ + return false; + } + } + } diff --git a/apps/testing/src/main/java/com/akto/test_editor/execution/Executor.java b/apps/testing/src/main/java/com/akto/test_editor/execution/Executor.java index fc18afa3a0..274bac613a 100644 --- a/apps/testing/src/main/java/com/akto/test_editor/execution/Executor.java +++ b/apps/testing/src/main/java/com/akto/test_editor/execution/Executor.java @@ -41,6 +41,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import org.json.JSONObject; @@ -432,9 +433,55 @@ private ExecutorSingleOperationResp modifyAuthTokenInRawApi(TestRoles testRole, return null; } + private static BasicDBObject getBillingTokenForAuth() { + BasicDBObject bDObject; + int accountId = Context.accountId.get(); + Organization organization = OrganizationsDao.instance.findOne( + Filters.in(Organization.ACCOUNTS, accountId) + ); + if (organization == null) { + return new BasicDBObject("error", "organization not found"); + } + + Tokens tokens; + Bson filters = Filters.and( + Filters.eq(Tokens.ORG_ID, organization.getId()), + Filters.eq(Tokens.ACCOUNT_ID, accountId) + ); + String errMessage = ""; + tokens = TokensDao.instance.findOne(filters); + if (tokens == null) { + errMessage = "error extracting ${akto_header}, token is missing"; + } + if (tokens.isOldToken()) { + errMessage = "error extracting ${akto_header}, token is old"; + } + if(errMessage.length() > 0){ + bDObject = new BasicDBObject("error", errMessage); + }else{ + bDObject = new BasicDBObject("token", tokens.getToken()); + } + return bDObject; + } public ExecutorSingleOperationResp runOperation(String operationType, RawApi rawApi, Object key, Object value, Map varMap, AuthMechanism authMechanism, List customAuthTypes) { switch (operationType.toLowerCase()) { + case "send_ssrf_req": + String keyValue = key.toString().replaceAll("\\$\\{random_uuid\\}", ""); + String url = Utils.extractValue(keyValue, "url="); + String redirectUrl = Utils.extractValue(keyValue, "redirect_url="); + List uuidList = (List) varMap.getOrDefault("random_uuid", new ArrayList<>()); + String generatedUUID = UUID.randomUUID().toString(); + uuidList.add(generatedUUID); + varMap.put("random_uuid", uuidList); + + BasicDBObject response = getBillingTokenForAuth(); + if(response.getString("token") != null){ + String tokenVal = response.getString("token"); + return Utils.sendRequestToSsrfServer(url + generatedUUID, redirectUrl, tokenVal); + }else{ + return new ExecutorSingleOperationResp(false, response.getString("error")); + } case "attach_file": return Operations.addHeader(rawApi, Constants.AKTO_ATTACH_FILE , key.toString()); case "add_body_param": @@ -458,27 +505,12 @@ public ExecutorSingleOperationResp runOperation(String operationType, RawApi raw return Operations.replaceBody(rawApi, newPayload); case "add_header": if (value.equals("${akto_header}")) { - int accountId = Context.accountId.get(); - Organization organization = OrganizationsDao.instance.findOne( - Filters.in(Organization.ACCOUNTS, accountId) - ); - if (organization == null) { - return new ExecutorSingleOperationResp(false, "accountId " + accountId + " isn't associated with any organization"); - } - - Tokens tokens; - Bson filters = Filters.and( - Filters.eq(Tokens.ORG_ID, organization.getId()), - Filters.eq(Tokens.ACCOUNT_ID, accountId) - ); - tokens = TokensDao.instance.findOne(filters); - if (tokens == null) { - return new ExecutorSingleOperationResp(false, "error extracting ${akto_header}, token is missing"); - } - if (tokens.isOldToken()) { - return new ExecutorSingleOperationResp(false, "error extracting ${akto_header}, token is old"); + BasicDBObject tokenResponse = getBillingTokenForAuth(); + if(tokenResponse.getString("token") != null){ + value = tokenResponse.getString("token"); + }else{ + return new ExecutorSingleOperationResp(false, tokenResponse.getString("error")); } - value = tokens.getToken(); } return Operations.addHeader(rawApi, key.toString(), value.toString()); diff --git a/apps/testing/src/main/java/com/akto/test_editor/execution/Operations.java b/apps/testing/src/main/java/com/akto/test_editor/execution/Operations.java index 171fa3c915..fbe321be74 100644 --- a/apps/testing/src/main/java/com/akto/test_editor/execution/Operations.java +++ b/apps/testing/src/main/java/com/akto/test_editor/execution/Operations.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Map; +import org.yaml.snakeyaml.scanner.Constant; + import com.akto.dto.RawApi; import com.akto.dto.test_editor.ExecutorSingleOperationResp; import com.akto.test_editor.Utils; diff --git a/apps/testing/src/main/java/com/akto/test_editor/execution/VariableResolver.java b/apps/testing/src/main/java/com/akto/test_editor/execution/VariableResolver.java index 9234cdc895..eab651f89d 100644 --- a/apps/testing/src/main/java/com/akto/test_editor/execution/VariableResolver.java +++ b/apps/testing/src/main/java/com/akto/test_editor/execution/VariableResolver.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -121,6 +122,7 @@ public static List resolveExpression(Map varMap, String String match = matcher.group(0); match = match.substring(2, match.length()); match = match.substring(0, match.length() - 1); + Object val = getValue(varMap, match); if (val == null) { continue; @@ -805,4 +807,4 @@ public static Object multiply(String operand1, String operand2) { // } -} +} \ No newline at end of file diff --git a/apps/testing/src/main/java/com/akto/test_editor/filter/Filter.java b/apps/testing/src/main/java/com/akto/test_editor/filter/Filter.java index 7c7fd56b5f..5ad2e8a67f 100644 --- a/apps/testing/src/main/java/com/akto/test_editor/filter/Filter.java +++ b/apps/testing/src/main/java/com/akto/test_editor/filter/Filter.java @@ -42,6 +42,13 @@ public DataOperandsFilterResponse isEndpointValid(FilterNode node, RawApi rawApi Boolean res = filterAction.invokeFilter(dataOperandFilterRequest); return new DataOperandsFilterResponse(res, matchingKeySet, contextEntities, null); } + if (node.getOperand().equalsIgnoreCase(TestEditorEnums.PredicateOperator.SSRF_URL_HIT.toString())) { + Object updatedQuerySet = filterAction.resolveQuerySetValues(null, node.fetchNodeValues(), varMap); + List val = (List) updatedQuerySet; + DataOperandFilterRequest dataOperandFilterRequest = new DataOperandFilterRequest(null, val, "ssrf_url_hit"); + Boolean res = filterAction.invokeFilter(dataOperandFilterRequest); + return new DataOperandsFilterResponse(res, matchingKeySet, contextEntities, null); + } if (! (node.getNodeType().toLowerCase().equals(OperandTypes.Data.toString().toLowerCase()) || node.getNodeType().toLowerCase().equals(OperandTypes.Extract.toString().toLowerCase()) || node.getNodeType().toLowerCase().equals(OperandTypes.Context.toString().toLowerCase() ))) { return new DataOperandsFilterResponse(false, null, null, null); } diff --git a/apps/testing/src/main/java/com/akto/test_editor/filter/FilterAction.java b/apps/testing/src/main/java/com/akto/test_editor/filter/FilterAction.java index 02ebd00438..66cba5f2ca 100644 --- a/apps/testing/src/main/java/com/akto/test_editor/filter/FilterAction.java +++ b/apps/testing/src/main/java/com/akto/test_editor/filter/FilterAction.java @@ -61,6 +61,7 @@ public final class FilterAction { put("contains_jwt", new ContainsJwt()); put("cookie_expire_filter", new CookieExpireFilter()); put("datatype", new DatatypeFilter()); + put("ssrf_url_hit", new SsrfUrlHitFilter()); }}; public FilterAction() { } diff --git a/apps/testing/src/main/java/com/akto/test_editor/filter/data_operands_impl/SsrfUrlHitFilter.java b/apps/testing/src/main/java/com/akto/test_editor/filter/data_operands_impl/SsrfUrlHitFilter.java new file mode 100644 index 0000000000..5d8ca68a66 --- /dev/null +++ b/apps/testing/src/main/java/com/akto/test_editor/filter/data_operands_impl/SsrfUrlHitFilter.java @@ -0,0 +1,34 @@ +package com.akto.test_editor.filter.data_operands_impl; + +import java.util.ArrayList; +import java.util.List; + +import com.akto.dto.test_editor.DataOperandFilterRequest; +import com.akto.test_editor.Utils; + +public class SsrfUrlHitFilter extends DataOperandsImpl { + + @Override + public Boolean isValid(DataOperandFilterRequest dataOperandFilterRequest) { + + Boolean result = false; + List querySet = new ArrayList<>(); + String data; + try { + querySet = (List) dataOperandFilterRequest.getQueryset(); + data = (String) dataOperandFilterRequest.getData(); + } catch(Exception e) { + return result; + } + + for (String queryString: querySet) { + if(Utils.sendRequestToSsrfServer(queryString)){ + result = true; + break; + } + } + + return result; + } + +} diff --git a/docker.env b/docker.env index f9e4d9cd4d..4c02a9279e 100644 --- a/docker.env +++ b/docker.env @@ -5,4 +5,5 @@ AKTO_TRAFFIC_BATCH_SIZE=100 AKTO_TRAFFIC_BATCH_TIME_SECS=10 DASHBOARD_MODE=local_deploy USE_HOSTNAME=true -PUPPETEER_REPLAY_SERVICE_URL=http://akto-puppeteer-replay:3000 \ No newline at end of file +PUPPETEER_REPLAY_SERVICE_URL=http://akto-puppeteer-replay:3000 +SSRF_SERVICE_NAME="https://test-services.akto.io/" \ No newline at end of file diff --git a/libs/dao/src/main/java/com/akto/dao/test_editor/TestEditorEnums.java b/libs/dao/src/main/java/com/akto/dao/test_editor/TestEditorEnums.java index 098aa53af1..9b8b732a69 100644 --- a/libs/dao/src/main/java/com/akto/dao/test_editor/TestEditorEnums.java +++ b/libs/dao/src/main/java/com/akto/dao/test_editor/TestEditorEnums.java @@ -40,7 +40,8 @@ public enum TermOperands { public enum PredicateOperator { AND, OR, - COMPARE_GREATER + COMPARE_GREATER, + SSRF_URL_HIT } public enum KeyValOperator { @@ -204,7 +205,8 @@ public enum TerminalExecutorDataOperands { REPLACE_AUTH_HEADER, REPLACE_BODY, JWT_REPLACE_BODY, - ATTACH_FILE + ATTACH_FILE, + SEND_SSRF_REQ, } public enum NonTerminalExecutorDataOperands { diff --git a/libs/dao/src/main/java/com/akto/dao/test_editor/filter/ConfigParser.java b/libs/dao/src/main/java/com/akto/dao/test_editor/filter/ConfigParser.java index 997c641de2..3ec8774e51 100644 --- a/libs/dao/src/main/java/com/akto/dao/test_editor/filter/ConfigParser.java +++ b/libs/dao/src/main/java/com/akto/dao/test_editor/filter/ConfigParser.java @@ -44,6 +44,10 @@ public ConfigParserResult validateAndTransform(Map filters, Filt Object values = curNode.getValues(); ConfigParserValidationResult configParserValidationResult = validateNodeAgainstRules(curNode, parentNode, termNodeExists, collectionNodeExists, concernedProperty, contextProperty); + if (curNode.getOperand().equalsIgnoreCase(PredicateOperator.SSRF_URL_HIT.toString())) { + return new ConfigParserResult(null, true, ""); + } + if (!configParserValidationResult.getIsValid()) { return new ConfigParserResult(null, false, configParserValidationResult.getErrMsg()); } @@ -189,7 +193,7 @@ public ConfigParserValidationResult validateNodeAgainstRules(FilterNode curNode, } // 5. Last Node should always be a data/extract node - if (! (curNodeType.equals(OperandTypes.Data.toString().toLowerCase()) || curNodeType.equals(OperandTypes.Extract.toString().toLowerCase()) || curNodeType.equals(OperandTypes.Context.toString().toLowerCase()) || curNode.getOperand().equalsIgnoreCase(TestEditorEnums.PredicateOperator.COMPARE_GREATER.toString()))) { + if (! (curNodeType.equals(OperandTypes.Data.toString().toLowerCase()) || curNodeType.equals(OperandTypes.Extract.toString().toLowerCase()) || curNodeType.equals(OperandTypes.Context.toString().toLowerCase()) || curNode.getOperand().equalsIgnoreCase(TestEditorEnums.PredicateOperator.COMPARE_GREATER.toString()) || curNode.getOperand().equalsIgnoreCase(TestEditorEnums.PredicateOperator.COMPARE_GREATER.toString()))) { if (isString(values) || isListOfString(values)) { configParserValidationResult.setIsValid(false); configParserValidationResult.setErrMsg("Last Node should always be a data/extract node"); diff --git a/libs/dao/src/main/java/com/akto/util/Constants.java b/libs/dao/src/main/java/com/akto/util/Constants.java index ae700ea9ab..2649f8352a 100644 --- a/libs/dao/src/main/java/com/akto/util/Constants.java +++ b/libs/dao/src/main/java/com/akto/util/Constants.java @@ -13,5 +13,6 @@ private Constants() {} public static final String AKTO_IGNORE_FLAG = "x-akto-ignore"; public static final String AKTO_ATTACH_FILE = "x-akto-attach-file"; + public static final String AKTO_TOKEN_KEY = "x-akto-key"; }