core.api.DeniedColumns.rejected(...) returns early when parsed.type() == QueryType.OTHER, on the premise that no permission grants OTHER. That premise is false for request groups: DefaultRequestGroupService.validatePermission admits an OTHER member (MERGE, CALL, …) for a non-admin holding only can_write (break-glass groups: can_break_glass), and verifyTableAndColumnScope then calls DeniedColumns.rejected, which skips it.
So MERGE INTO t USING (SELECT ssn FROM users) s ON … WHEN MATCHED THEN UPDATE SET x = s.ssn bypasses a denied_columns = [users.ssn] grant.
Fix: OTHER is never column-analysed by SqlParserServiceImpl, so it must fail closed like any unanalysed parse — an OTHER statement reaches every denied entry. Same approach as DeniedShapes.rejected in #1101 (#940).
core.api.DeniedColumns.rejected(...)returns early whenparsed.type() == QueryType.OTHER, on the premise that no permission grants OTHER. That premise is false for request groups:DefaultRequestGroupService.validatePermissionadmits an OTHER member (MERGE,CALL, …) for a non-admin holding onlycan_write(break-glass groups:can_break_glass), andverifyTableAndColumnScopethen callsDeniedColumns.rejected, which skips it.So
MERGE INTO t USING (SELECT ssn FROM users) s ON … WHEN MATCHED THEN UPDATE SET x = s.ssnbypasses adenied_columns = [users.ssn]grant.Fix: OTHER is never column-analysed by
SqlParserServiceImpl, so it must fail closed like any unanalysed parse — an OTHER statement reaches every denied entry. Same approach asDeniedShapes.rejectedin #1101 (#940).