Skip to content

Commit

Permalink
Merge pull request #949 from akto-api-security/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
notshivansh authored Mar 21, 2024
2 parents 7893dcf + 0fb464a commit 2594d41
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 25 deletions.
16 changes: 16 additions & 0 deletions apps/dashboard/src/main/resources/struts.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2864,23 +2864,39 @@
<action name="api/testIntegration" class="com.akto.action.JiraIntegrationAction" method="testIntegration" >
<interceptor-ref name="json"/>
<interceptor-ref name="defaultStack" />
<interceptor-ref name="usageInterceptor">
<param name="featureLabel">JIRA_INTEGRATION</param>
</interceptor-ref>
<result name="SUCCESS" type="json"/>
<result name="ERROR" type="json">
<param name="statusCode">422</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*, ^responses.*</param>
</result>
<result name="UNAUTHORIZED" type="json">
<param name="statusCode">403</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*</param>
</result>
</action>

<action name="api/addIntegration" class="com.akto.action.JiraIntegrationAction" method="addIntegration" >
<interceptor-ref name="json"/>
<interceptor-ref name="defaultStack" />
<interceptor-ref name="usageInterceptor">
<param name="featureLabel">JIRA_INTEGRATION</param>
</interceptor-ref>
<result name="SUCCESS" type="json"/>
<result name="ERROR" type="json">
<param name="statusCode">422</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*, ^responses.*</param>
</result>
<result name="UNAUTHORIZED" type="json">
<param name="statusCode">403</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*</param>
</result>
</action>

<action name="api/fetchIntegration" class="com.akto.action.JiraIntegrationAction" method="fetchIntegration" >
Expand Down
72 changes: 72 additions & 0 deletions apps/testing/src/main/java/com/akto/test_editor/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +31,8 @@
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;

import okhttp3.*;

public class Utils {

private static final ObjectMapper mapper = new ObjectMapper();
Expand Down Expand Up @@ -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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.json.JSONObject;

Expand Down Expand Up @@ -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<String, Object> varMap, AuthMechanism authMechanism, List<CustomAuthType> 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<String> uuidList = (List<String>) 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":
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -121,6 +122,7 @@ public static List<Object> resolveExpression(Map<String, Object> 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;
Expand Down Expand Up @@ -805,4 +807,4 @@ public static Object multiply(String operand1, String operand2) {

// }

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object> val = (List<Object>) 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> querySet = new ArrayList<>();
String data;
try {
querySet = (List<String>) dataOperandFilterRequest.getQueryset();
data = (String) dataOperandFilterRequest.getData();
} catch(Exception e) {
return result;
}

for (String queryString: querySet) {
if(Utils.sendRequestToSsrfServer(queryString)){
result = true;
break;
}
}

return result;
}

}
3 changes: 2 additions & 1 deletion docker.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
PUPPETEER_REPLAY_SERVICE_URL=http://akto-puppeteer-replay:3000
SSRF_SERVICE_NAME="https://test-services.akto.io/"
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public enum TermOperands {
public enum PredicateOperator {
AND,
OR,
COMPARE_GREATER
COMPARE_GREATER,
SSRF_URL_HIT
}

public enum KeyValOperator {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public ConfigParserResult validateAndTransform(Map<String, Object> 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());
}
Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions libs/dao/src/main/java/com/akto/util/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}

0 comments on commit 2594d41

Please sign in to comment.