@@ -40,14 +40,22 @@ public class ConnectionChatAccessPolicyService {
4040 "access\\ s+only\\ s+to\\ s+(?:schema\\ s+)?([a-z_][a-z0-9_]*)" ,
4141 Pattern .CASE_INSENSITIVE
4242 );
43- private static final Pattern DENY_CLAUSE_PATTERN = Pattern .compile (
44- "(?:cannot|can't|must not|do not|don't|never)\\ s+(?:query|access|see|select|read|use|return|expose)\\ s+(.+?)(?=\\ s+(?:but|except|however|strictly)\\ b|[.;]|$)"
45- + "|(?:redact|block|deny|hide)\\ s+(.+?)(?=\\ s+(?:but|except|however)\\ b|[.;]|$)" ,
46- Pattern .CASE_INSENSITIVE | Pattern .DOTALL
43+ // Prefix-only: remainder is sliced linearly in extractConstraints so we never
44+ // run `.+?` + `\s+` lookaheads on untrusted policy text (ReDoS / CodeQL).
45+ private static final Pattern DENY_PREFIX_PATTERN = Pattern .compile (
46+ "(?:cannot|can't|must not|do not|don't|never) (?:query|access|see|select|read|use|return|expose) "
47+ + "|(?:redact|block|deny|hide) " ,
48+ Pattern .CASE_INSENSITIVE
49+ );
50+ private static final Pattern ALLOW_PREFIX_PATTERN = Pattern .compile (
51+ "(?:but |except(?: that)? )?\\ bcan (?:query|access|see|select|read|use|return) " ,
52+ Pattern .CASE_INSENSITIVE
4753 );
48- private static final Pattern ALLOW_CLAUSE_PATTERN = Pattern .compile (
49- "(?:but\\ s+|except(?:\\ s+that)?\\ s+)?\\ bcan\\ s+(?:query|access|see|select|read|use|return)\\ s+(.+?)(?=\\ s+(?:strictly)\\ b|[.;]|$)" ,
50- Pattern .CASE_INSENSITIVE | Pattern .DOTALL
54+ private static final Pattern DENY_STOP_PATTERN = Pattern .compile (
55+ " (?:but|except|however|strictly)\\ b|[.;]"
56+ );
57+ private static final Pattern ALLOW_STOP_PATTERN = Pattern .compile (
58+ " (?:strictly)\\ b|[.;]"
5159 );
5260 private static final List <TypeFamily > TYPE_FAMILIES = List .of (
5361 new TypeFamily ("integer" , Set .of ("int" , "integer" , "bigint" , "smallint" , "tinyint" , "serial" , "bigserial" , "int2" , "int4" , "int8" )),
@@ -418,8 +426,8 @@ private void applyColumnConstraints(
418426 Set <String > deniedColumns
419427 ) {
420428 Set <String > knownColumnNames = collectColumnNames (schemaMetadata , allowedSchemas );
421- List <ColumnConstraint > denials = extractConstraints (normalized , DENY_CLAUSE_PATTERN , knownColumnNames );
422- List <ColumnConstraint > allowances = extractConstraints (normalized , ALLOW_CLAUSE_PATTERN , knownColumnNames );
429+ List <ColumnConstraint > denials = extractConstraints (normalized , DENY_PREFIX_PATTERN , DENY_STOP_PATTERN , knownColumnNames );
430+ List <ColumnConstraint > allowances = extractConstraints (normalized , ALLOW_PREFIX_PATTERN , ALLOW_STOP_PATTERN , knownColumnNames );
423431 if (denials .isEmpty ()) {
424432 return ;
425433 }
@@ -439,24 +447,31 @@ private void applyColumnConstraints(
439447 }
440448 }
441449
442- private List <ColumnConstraint > extractConstraints (String normalized , Pattern clausePattern , Set <String > knownColumnNames ) {
450+ private List <ColumnConstraint > extractConstraints (
451+ String normalized ,
452+ Pattern prefixPattern ,
453+ Pattern stopPattern ,
454+ Set <String > knownColumnNames
455+ ) {
443456 List <ColumnConstraint > constraints = new ArrayList <>();
444- Matcher matcher = clausePattern .matcher (normalized );
457+ String haystack = normalized == null ? "" : normalized .replaceAll ("\\ s+" , " " ).trim ();
458+ Matcher matcher = prefixPattern .matcher (haystack );
445459 while (matcher .find ()) {
446- String snippet = firstNonBlank ( matcher );
447- if (snippet == null ) {
460+ String snippet = sliceUntilStop ( haystack , matcher . end (), stopPattern );
461+ if (snippet == null || snippet . isBlank () ) {
448462 continue ;
449463 }
464+ String lowered = snippet .toLowerCase (Locale .ROOT );
450465 LinkedHashSet <String > typeKeys = new LinkedHashSet <>();
451466 for (TypeFamily family : TYPE_FAMILIES ) {
452- if (family .mentionedIn (snippet )) {
467+ if (family .mentionedIn (lowered )) {
453468 typeKeys .add (family .key ());
454469 }
455470 }
456471 LinkedHashSet <String > nameTokens = new LinkedHashSet <>();
457472 knownColumnNames .stream ()
458473 .sorted ((left , right ) -> Integer .compare (right .length (), left .length ()))
459- .filter (name -> containsWholeWord (snippet , name ))
474+ .filter (name -> containsWholeWord (lowered , name ))
460475 .forEach (nameTokens ::add );
461476 if (!typeKeys .isEmpty () || !nameTokens .isEmpty ()) {
462477 constraints .add (new ColumnConstraint (typeKeys , nameTokens ));
@@ -488,14 +503,15 @@ private boolean isTypeToken(String name) {
488503 return TYPE_FAMILIES .stream ().anyMatch (family -> family .aliases ().contains (name ) || family .key ().equals (name ));
489504 }
490505
491- private String firstNonBlank (Matcher matcher ) {
492- for (int i = 1 ; i <= matcher .groupCount (); i ++) {
493- String group = matcher .group (i );
494- if (group != null && !group .isBlank ()) {
495- return group .toLowerCase (Locale .ROOT );
496- }
506+ private String sliceUntilStop (String haystack , int start , Pattern stopPattern ) {
507+ if (start >= haystack .length ()) {
508+ return "" ;
509+ }
510+ Matcher stop = stopPattern .matcher (haystack );
511+ if (stop .find (start )) {
512+ return haystack .substring (start , stop .start ()).trim ();
497513 }
498- return null ;
514+ return haystack . substring ( start ). trim () ;
499515 }
500516
501517 private boolean containsWholeWord (String haystack , String needle ) {
0 commit comments