Skip to content

Commit d992f17

Browse files
dkayiwaclaude
andauthored
Bind caller-supplied names as query parameters instead of concatenating (#6335)
* Bind the program-attribute name as a query parameter getPatientProgramAttributeByAttributeName assembled its native query by concatenating the attributeName argument straight into the SQL text. Bind it as a named parameter (:attributeName) instead so the value is always handled as a literal attribute-type name and can never alter the query. The patient-id list stays inlined since those elements are ints. Adds a regression test that a name containing SQL control characters is matched literally and resolves no attribute type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpKNfxm3QjwUBDePEtFm2H * Bind the global-property name in FilterUtil.getGlobalPropertyValue Replace the concatenated global_property lookup with a PreparedStatement so the property name is always treated as a literal value rather than being spliced into the SQL text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add regression test for FilterUtil.getGlobalPropertyValue name binding Guards that the property name is matched literally: a name carrying SQL control characters resolves no property (returns null) instead of being interpreted as query text, which the earlier concatenated query allowed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b7941b8 commit d992f17

4 files changed

Lines changed: 64 additions & 9 deletions

File tree

api/src/main/java/org/openmrs/api/db/hibernate/HibernateProgramWorkflowDAO.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,12 +475,12 @@ public Map<Object, Object> getPatientProgramAttributeByAttributeName(List<Intege
475475
+ " from patient p " + " join patient_program pp on p.patient_id = pp.patient_id and p.patient_id in ("
476476
+ commaSeperatedPatientIds + ")"
477477
+ " join patient_program_attribute ppa on pp.patient_program_id = ppa.patient_program_id and ppa.voided=0"
478-
+ " join program_attribute_type ppt on ppa.attribute_type_id = ppt.program_attribute_type_id and ppt.name ='"
479-
+ attributeName + "' "
478+
+ " join program_attribute_type ppt on ppa.attribute_type_id = ppt.program_attribute_type_id and ppt.name = :attributeName "
480479
+ " LEFT OUTER JOIN concept_name cn on ppa.value_reference = cn.concept_id and cn.concept_name_type= 'FULLY_SPECIFIED' and cn.voided=0 and ppt.datatype like '%ConceptDataType%'"
481480
+ " group by p.patient_id",
482481
Object.class).addScalar("person_id", StandardBasicTypes.INTEGER)
483-
.addScalar("patientProgramAttributeValue", StandardBasicTypes.STRING).list();
482+
.addScalar("patientProgramAttributeValue", StandardBasicTypes.STRING)
483+
.setParameter("attributeName", attributeName).list();
484484

485485
for (Object o : list) {
486486
Object[] arr = (Object[]) o;

api/src/test/java/org/openmrs/api/ProgramWorkflowServiceTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Date;
1616
import java.util.HashSet;
1717
import java.util.List;
18+
import java.util.Map;
1819
import java.util.Set;
1920

2021
import org.junit.jupiter.api.BeforeEach;
@@ -26,6 +27,7 @@
2627
import org.openmrs.Encounter;
2728
import org.openmrs.Patient;
2829
import org.openmrs.PatientProgram;
30+
import org.openmrs.PatientProgramAttribute;
2931
import org.openmrs.PatientState;
3032
import org.openmrs.Program;
3133
import org.openmrs.ProgramAttributeType;
@@ -1117,4 +1119,40 @@ public void triggerStateConversion_shouldTestTransitionToState() {
11171119
// return props;
11181120
// }
11191121

1122+
/**
1123+
* Regression guard for the program-attribute lookup: the attribute name is bound as a query
1124+
* parameter rather than concatenated into the native SQL, so a value carrying SQL control
1125+
* characters is matched literally instead of altering the query.
1126+
*
1127+
* @see ProgramWorkflowService#getPatientProgramAttributeByAttributeName(List, String)
1128+
*/
1129+
@Test
1130+
public void getPatientProgramAttributeByAttributeName_shouldMatchTheAttributeNameLiterally() {
1131+
// attach a "ProgramId" attribute value to the existing enrollment for patient 2
1132+
ProgramAttributeType attributeType = pws.getProgramAttributeType(1);
1133+
assertEquals("ProgramId", attributeType.getName());
1134+
1135+
PatientProgramAttribute attribute = new PatientProgramAttribute();
1136+
attribute.setAttributeType(attributeType);
1137+
attribute.setValueReferenceInternal("1234");
1138+
1139+
PatientProgram patientProgram = pws.getPatientProgram(1);
1140+
patientProgram.addAttribute(attribute);
1141+
pws.savePatientProgram(patientProgram);
1142+
Context.flushSession();
1143+
1144+
List<Integer> patientIds = Arrays.asList(2);
1145+
1146+
// the real name resolves the stored value
1147+
Map<Object, Object> byName = pws.getPatientProgramAttributeByAttributeName(patientIds, "ProgramId");
1148+
assertTrue(byName.containsKey(2));
1149+
assertTrue(((String) byName.get(2)).contains("1234"));
1150+
1151+
// a name containing SQL quote characters is compared literally, so it resolves no attribute
1152+
// type and returns nothing, rather than being interpreted as part of the query
1153+
Map<Object, Object> byNameWithQuotes = pws.getPatientProgramAttributeByAttributeName(patientIds,
1154+
"ProgramId' OR '1'='1");
1155+
assertTrue(byNameWithQuotes.isEmpty());
1156+
}
1157+
11201158
}

web/src/main/java/org/openmrs/web/filter/util/FilterUtil.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@
1313
import java.sql.PreparedStatement;
1414
import java.sql.ResultSet;
1515
import java.sql.SQLException;
16-
import java.util.List;
1716

1817
import org.apache.commons.lang3.StringUtils;
1918
import org.openmrs.util.DatabaseUpdater;
20-
import org.openmrs.util.DatabaseUtil;
2119
import org.openmrs.util.OpenmrsConstants;
2220
import org.slf4j.Logger;
2321
import org.slf4j.LoggerFactory;
@@ -268,10 +266,14 @@ public static String getGlobalPropertyValue(String globalPropertyName) {
268266

269267
try {
270268
connection = DatabaseUpdater.getConnection();
271-
List<List<Object>> results = DatabaseUtil.executeSQL(connection,
272-
"select property_value from global_property where property = '" + globalPropertyName + "'", true);
273-
if (results.size() == 1 && results.get(0).size() == 1) {
274-
propertyValue = results.get(0).get(0).toString();
269+
try (PreparedStatement statement = connection
270+
.prepareStatement("select property_value from global_property where property = ?")) {
271+
statement.setString(1, globalPropertyName);
272+
try (ResultSet resultSet = statement.executeQuery()) {
273+
if (resultSet.next()) {
274+
propertyValue = resultSet.getString(1);
275+
}
276+
}
275277
}
276278
} catch (Exception e) {
277279
log.error("Error while retrieving value for global property:" + globalPropertyName, e);

web/src/test/java/org/openmrs/web/filter/update/util/FilterUtilTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
1717
import static org.junit.jupiter.api.Assertions.assertEquals;
1818
import static org.junit.jupiter.api.Assertions.assertNotEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNull;
1920

2021
/**
2122
* Tests some of the methods on the {@link FilterUtil}
@@ -61,4 +62,18 @@ public void storeLocale_shouldNotUpdateStoredValueWhenStoringDifferentLocale() {
6162
assertNotEquals(UPDATED_LOCALE, FilterUtil.restoreLocale(FilterUtil.ADMIN_USERNAME));
6263
}
6364

65+
/**
66+
* @see FilterUtil#getGlobalPropertyValue(String)
67+
*/
68+
@Test
69+
public void getGlobalPropertyValue_shouldMatchThePropertyNameLiterally() {
70+
// a real property name resolves its stored value
71+
assertEquals("7", FilterUtil.getGlobalPropertyValue("concept.true"));
72+
73+
// a name carrying SQL control characters is bound as a literal, so it matches no property
74+
// and returns null. On the earlier string-concatenated query this payload resolved a single
75+
// row and returned "7", so this asserts the value is now treated as data, not query text.
76+
assertNull(FilterUtil.getGlobalPropertyValue("nonexistent' OR property = 'concept.true"));
77+
}
78+
6479
}

0 commit comments

Comments
 (0)