Skip to content

Commit 9d952ee

Browse files
authored
Merge pull request #5661 from pauline2k/boac-6760
BOAC-6760 Additional validation/parameterization for SQL and LDAP queries
2 parents a180ef6 + b0a854c commit 9d952ee

6 files changed

Lines changed: 55 additions & 30 deletions

File tree

boac/api/peer_advising_notes_controller.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,11 @@ def get_notes_authored_by():
250250
# The 'timeframe' param (optional) has two properties: year and month.
251251
timeframe = params.get('timeframe') or None
252252
if timeframe:
253-
month = timeframe['month']
254-
year = timeframe['year']
253+
try:
254+
month = int(timeframe['month'])
255+
year = int(timeframe['year'])
256+
except (TypeError, ValueError):
257+
raise BadRequestError('Invalid timeframe')
255258
timeframe = f"{year}-{f'0{month}' if month < 10 else month}"
256259
notes = []
257260
rows = Note.get_peer_advising_notes_authored_by(

boac/api/search_controller.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def find_advisors_by_name():
232232
query = request.args.get('q')
233233
if not query:
234234
raise BadRequestError('Search query must be supplied')
235-
limit = request.args.get('limit')
235+
limit = request.args.get('limit', type=int)
236236
query_fragments = list(filter(None, set(query.upper().split(' '))))
237237
advisors = _advisors_by_name(query_fragments, limit=limit)
238238
legacy_note_authors = match_advising_note_authors_by_name(query_fragments, limit=limit)
@@ -261,7 +261,8 @@ def _advisors_by_name(tokens, limit=None):
261261
{' '.join(token_conditions)}
262262
ORDER BY a.advisor_name"""
263263
if limit:
264-
sql += f' LIMIT {limit}'
264+
sql += ' LIMIT :limit'
265+
params['limit'] = limit
265266
benchmark('execute query')
266267
results = db.session.execute(text(sql), params)
267268
benchmark('end')
@@ -402,7 +403,7 @@ def _student_search(search_phrase, params, order_by):
402403
student_results = search_for_students(
403404
search_phrase=search_phrase.replace(',', ' '),
404405
order_by=order_by,
405-
offset=util.get(params, 'offset', 0),
406+
offset=int(util.get(params, 'offset', 0)),
406407
limit=util.get(params, 'limit', 50),
407408
)
408409
students = student_results['students']

boac/externals/calnet.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def connect(self):
7070
return ldap3.Connection(self.server, user=self.bind, password=self.password, auto_bind=ldap3.AUTO_BIND_TLS_BEFORE_BIND)
7171

7272
def search_csids(self, csids, search_expired=False):
73+
csids = [csid for csid in csids if str(csid).isdigit()]
7374
all_out = []
7475
for i in range(0, len(csids), BATCH_QUERY_MAXIMUM):
7576
csids_batch = csids[i:i + BATCH_QUERY_MAXIMUM]
@@ -80,6 +81,7 @@ def search_csids(self, csids, search_expired=False):
8081
return all_out
8182

8283
def search_uids(self, uids, search_expired=False):
84+
uids = [uid for uid in uids if str(uid).isdigit()]
8385
all_out = []
8486
for i in range(0, len(uids), BATCH_QUERY_MAXIMUM):
8587
uids_batch = uids[i:i + BATCH_QUERY_MAXIMUM]
@@ -91,7 +93,8 @@ def search_uids(self, uids, search_expired=False):
9193

9294
@classmethod
9395
def _ldap_search_filter(cls, ids, id_type, search_expired=False):
94-
ids_filter = ''.join(f'({id_type}={_id})' for _id in ids)
96+
numeric_ids = [_id for _id in ids if str(_id).isdigit()]
97+
ids_filter = ''.join(f'({id_type}={_id})' for _id in numeric_ids)
9598
ou_scope = '(ou=expired people)' if search_expired else '(ou=people) (ou=advcon people)'
9699
return f"""(&
97100
(objectclass=person)

boac/externals/data_loch.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,8 @@ def match_appointment_advisors_by_name(prefixes, limit=None):
501501
{' '.join(prefix_conditions)}
502502
ORDER BY a.first_name, a.last_name"""
503503
if limit:
504-
sql += f' LIMIT {limit}'
504+
sql += ' LIMIT %(limit)s'
505+
prefix_kwargs['limit'] = limit
505506
return safe_execute_rds(sql, **prefix_kwargs)
506507

507508

@@ -520,7 +521,8 @@ def match_advising_note_authors_by_name(prefixes, limit=None):
520521
{' '.join(prefix_conditions)}
521522
ORDER BY a.first_name, a.last_name"""
522523
if limit:
523-
sql += f' LIMIT {limit}'
524+
sql += ' LIMIT %(limit)s'
525+
prefix_kwargs['limit'] = limit
524526
return safe_execute_rds(sql, **prefix_kwargs)
525527

526528

@@ -692,7 +694,7 @@ def get_admitted_student_by_sid(sid):
692694

693695

694696
def get_admitted_students_by_sids(offset, sids, limit=None, order_by='last_name'):
695-
limit_clause = f'LIMIT {limit}' if limit else ''
697+
limit_clause = 'LIMIT %(limit)s' if limit else ''
696698
sql = f"""
697699
SELECT a.applyuc_cpid, a.cs_empl_id AS sid, a.uid, s.uid AS student_uid,
698700
a.residency_category, a.freshman_or_transfer, a.admit_term, a.admit_status, a.current_sir, college, a.first_name, a.middle_name,
@@ -1495,7 +1497,7 @@ def get_students_query( # noqa: C901, PLR0912, PLR0913, PLR0915
14951497
query_bindings.update({'previous_term_id': previous_term_id(current_term_id)})
14961498
query_filter += _number_ranges_to_sql('spi.units', unit_ranges) if unit_ranges else ''
14971499
if last_name_ranges:
1498-
query_filter += _last_name_ranges_to_sql(last_name_ranges)
1500+
query_filter += _last_name_ranges_to_sql(last_name_ranges, query_bindings)
14991501
if degree_terms:
15001502
query_filter += ' AND sd.term_id = ANY(%(degree_terms)s)'
15011503
query_bindings.update({'degree_terms': degree_terms})
@@ -1713,10 +1715,11 @@ def get_students_ordering(term_id, order_by=None, group_codes=None, majors=None,
17131715
o = 'set.enrolled_units'
17141716
elif order_by and order_by.startswith('term_gpa_'):
17151717
gpa_term_id = order_by.replace('term_gpa_', '')
1716-
supplemental_query_tables = f"""
1717-
LEFT JOIN {student_schema()}.student_enrollment_terms set
1718-
ON set.sid = spi.sid AND set.term_id = '{gpa_term_id}'"""
1719-
o = 'set.term_gpa'
1718+
if gpa_term_id.isdigit():
1719+
supplemental_query_tables = f"""
1720+
LEFT JOIN {student_schema()}.student_enrollment_terms set
1721+
ON set.sid = spi.sid AND set.term_id = '{gpa_term_id}'"""
1722+
o = 'set.term_gpa'
17201723
o_secondary = by_first_name if order_by == 'last_name' else by_last_name
17211724
diff = {by_first_name, by_last_name} - {o, o_secondary}
17221725
o_tertiary = diff.pop() if diff else 'spi.sid'
@@ -1840,9 +1843,9 @@ def _match_students_by_sid(sid, limit=None):
18401843
SELECT spi.first_name, spi.last_name, spi.email_address, spi.sid, spi.uid
18411844
FROM {student_schema()}.student_profile_index spi
18421845
WHERE spi.sid LIKE %(starts_with)s
1843-
{f' LIMIT {limit}' if limit else ''}
1846+
{'LIMIT %(limit)s' if limit else ''}
18441847
"""
1845-
return safe_execute_rds(sql, **{'starts_with': f'{sid}%'})
1848+
return safe_execute_rds(sql, **{'starts_with': f'{sid}%', 'limit': limit})
18461849

18471850

18481851
def _match_students_by_name_or_email(phrase, limit=None, prefix_only=False):
@@ -1864,12 +1867,13 @@ def _match_students_by_name_or_email(phrase, limit=None, prefix_only=False):
18641867
ELSE 2
18651868
END
18661869
), s.first_name, s.last_name
1867-
{f'LIMIT {limit}' if limit else ''}
1870+
{'LIMIT %(limit)s' if limit else ''}
18681871
) AS s
18691872
"""
18701873
return safe_execute_rds(sql, **{
18711874
'contains': f'%{phrase}%',
18721875
'starts_with': f'{phrase}%',
1876+
'limit': limit,
18731877
})
18741878

18751879

@@ -1918,8 +1922,9 @@ def _search_for_students(phrases, limit=None, prefix_only=False):
19181922
ELSE 2
19191923
END
19201924
), s.first_name, s.last_name
1921-
{f' LIMIT {limit}' if limit else ''}
1925+
{'LIMIT %(limit)s' if limit else ''}
19221926
"""
1927+
sql_params['limit'] = limit
19231928
return safe_execute_rds(sql, **sql_params)
19241929

19251930

@@ -1965,21 +1970,28 @@ def _number_ranges_to_sql(column, number_ranges):
19651970
return ''
19661971

19671972

1968-
def _last_name_ranges_to_sql(last_name_ranges):
1973+
def _last_name_ranges_to_sql(last_name_ranges, query_bindings):
19691974
query_filter = ''
19701975
count = len(last_name_ranges)
19711976
if count:
19721977
query_filter += ' AND ('
19731978
for idx, last_name_range in enumerate(last_name_ranges):
19741979
range_min = last_name_range['min'].upper()
19751980
range_max = last_name_range['max'].upper()
1981+
min_key = f'last_name_min_{idx}'
1982+
max_key = f'last_name_max_{idx}'
19761983
if range_max == range_min:
1977-
query_filter += f'(spi.last_name ILIKE \'{range_min}%%\')'
1984+
query_filter += f'(spi.last_name ILIKE %({min_key})s)'
1985+
query_bindings[min_key] = f'{range_min}%'
19781986
else:
1979-
query_filter += f'(UPPER(SUBSTRING(spi.last_name, 0, {len(range_min) + 1})) >= \'{range_min}\''
1987+
query_filter += f'(UPPER(SUBSTRING(spi.last_name, 0, %({min_key}_len)s)) >= %({min_key})s'
1988+
query_bindings[min_key] = range_min
1989+
query_bindings[f'{min_key}_len'] = len(range_min) + 1
19801990
if range_max < 'ZZ':
19811991
# If 'stop' were 'ZZ' then upper bound would not be necessary
1982-
query_filter += f' AND UPPER(SUBSTRING(spi.last_name, 0, {len(range_max) + 1})) <= \'{range_max}\''
1992+
query_filter += f' AND UPPER(SUBSTRING(spi.last_name, 0, %({max_key}_len)s)) <= %({max_key})s'
1993+
query_bindings[max_key] = range_max
1994+
query_bindings[f'{max_key}_len'] = len(range_max) + 1
19831995
query_filter += ')'
19841996
if idx < count - 1:
19851997
query_filter += ' OR '

boac/merged/student.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,8 @@ def search_for_students(
522522
ORDER BY MIN({o}) {o_direction} NULLS FIRST, MIN({o_secondary}) NULLS FIRST, MIN({o_tertiary}) NULLS FIRST"""
523523
if o_tertiary != 'spi.sid':
524524
sql += ', spi.sid'
525-
sql += f' OFFSET {offset}'
525+
sql += ' OFFSET %(offset)s'
526+
query_bindings['offset'] = offset
526527
if limit and limit < 100: # Sanity check large limits
527528
sql += ' LIMIT %(limit)s'
528529
query_bindings['limit'] = limit

boac/models/note.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,10 @@ def get_draft_note_count(cls, author_uid=None):
154154
count(*) FROM notes
155155
WHERE
156156
deleted_at IS NULL AND is_draft IS TRUE
157-
{ f"AND author_uid = '{author_uid}'" if author_uid else ''}
157+
{'AND author_uid = :author_uid' if author_uid else ''}
158158
"""
159-
return db.session.execute(text(sql)).mappings().first()['count']
159+
params = {'author_uid': author_uid} if author_uid else {}
160+
return db.session.execute(text(sql), params).mappings().first()['count']
160161

161162
@classmethod
162163
def get_draft_notes(cls, author_uid=None):
@@ -168,11 +169,12 @@ def get_draft_notes(cls, author_uid=None):
168169
WHERE
169170
n.is_draft IS TRUE
170171
AND n.deleted_at IS NULL
171-
{f"AND author_uid = '{author_uid}'" if author_uid else ''}
172+
{'AND author_uid = :author_uid' if author_uid else ''}
172173
GROUP BY n.id
173174
ORDER BY n.updated_at DESC
174175
"""
175-
for row in db.session.execute(text(sql)).mappings():
176+
params = {'author_uid': author_uid} if author_uid else {}
177+
for row in db.session.execute(text(sql), params).mappings():
176178
draft_notes.append({
177179
'id': row['id'],
178180
'attachmentCount': row['attachment_count'],
@@ -250,11 +252,13 @@ def get_peer_advising_notes_authored_by(
250252
AND n.is_private IS FALSE
251253
AND n.parent_note_id IS NULL
252254
AND n.peer_advising_department_id = :peer_advising_department_id
253-
{f" AND to_char(n.created_at, 'YYYY-MM') = '{timeframe_month}'" if timeframe_month else ''}
255+
{"AND to_char(n.created_at, 'YYYY-MM') = :timeframe_month" if timeframe_month else ''}
254256
GROUP BY n.id, a.id, t.topic
255257
ORDER BY n.updated_at DESC
256258
"""
257259
params = {'author_uid': author_uid, 'peer_advising_department_id': peer_advising_department_id}
260+
if timeframe_month:
261+
params['timeframe_month'] = timeframe_month
258262
return db.session.execute(text(sql), params).mappings().all()
259263

260264
@classmethod
@@ -596,10 +600,11 @@ def _fetch_result(is_count_query=False):
596600
{where_clause}
597601
"""
598602
if not is_count_query:
599-
sql += f"""
603+
sql += """
600604
ORDER BY notes.updated_at DESC, fts.rank DESC, notes.id DESC
601-
OFFSET {offset} LIMIT {limit}
605+
OFFSET :offset LIMIT :limit
602606
"""
607+
params.update({'offset': offset, 'limit': limit})
603608
return db.session.execute(text(sql), params).mappings()
604609
return {
605610
'results': [row for row in _fetch_result().all()],

0 commit comments

Comments
 (0)