fix: colon in query (#13176)

* fix: colon in query

* iteration
This commit is contained in:
Tom Moor
2026-07-27 22:37:15 -04:00
committed by GitHub
parent f97523f0b0
commit de032c01c1
2 changed files with 42 additions and 11 deletions
@@ -1420,5 +1420,23 @@ describe("PostgresSearchProvider", () => {
`"plugins"`
);
});
it("should not escape colons outside of a phrase", () => {
// pg-tsquery drops an unquoted colon but keeps a preceding backslash,
// so escaping here would leave a dangling "\" operand.
expect(
PostgresSearchProvider.webSearchQuery("hoffe, gehts gut :) bei")
).toBe("(hoffe|gehts&gut)&bei:*");
expect(PostgresSearchProvider.webSearchQuery("sub:title:here")).toBe(
"sub&title&here:*"
);
});
it("should escape colons inside a phrase", () => {
expect(PostgresSearchProvider.webSearchQuery(`"sub:title:here"`)).toBe(
`"sub\\:title\\:here"`
);
expect(PostgresSearchProvider.webSearchQuery(`'sub:title'`)).toBe(
`"sub\\:title":*`
);
});
});
});
@@ -840,9 +840,7 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
*/
public static webSearchQuery(query: string): string {
// limit length of search queries as we're using regex against untrusted input
let limitedQuery = PostgresSearchProvider.escapeQuery(
query.slice(0, PostgresSearchProvider.maxQueryLength)
);
let limitedQuery = query.slice(0, PostgresSearchProvider.maxQueryLength);
const quotedSearch =
limitedQuery.startsWith('"') && limitedQuery.endsWith('"');
@@ -867,6 +865,9 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
}
}
// Escape only once the phrase delimiters above have settled.
limitedQuery = PostgresSearchProvider.escapeTsQuery(limitedQuery);
return (
queryParser()(
// Although queryParser trims the query, looks like there's a
@@ -882,14 +883,26 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
}
private static escapeQuery(query: string): string {
return (
query
// replace "\" with escaped "\\" because sequelize.escape doesn't do it
// see: https://github.com/sequelize/sequelize/issues/2950
.replace(/\\/g, "\\\\")
// replace ":" with escaped "\:" because it's a reserved character in tsquery
// see: https://github.com/outline/outline/issues/6542
.replace(/:/g, "\\:")
// replace "\" with escaped "\\" because sequelize.escape doesn't do it
// see: https://github.com/sequelize/sequelize/issues/2950
return query.replace(/\\/g, "\\\\");
}
/**
* Escapes the characters that are reserved in tsquery, segment by segment so
* that quoted phrases can be treated differently to the rest of the query.
*
* ":" only needs escaping inside a phrase, where pg-tsquery passes it through
* verbatim (see: https://github.com/outline/outline/issues/6542). Elsewhere
* pg-tsquery drops the colon but keeps a preceding backslash, which would
* leave to_tsquery with a dangling escape character.
*/
private static escapeTsQuery(query: string): string {
return query.replace(/"[^"]*"|'[^']*'|[\s\S]/g, (segment) =>
// a match longer than the single character fallback is a quoted phrase
segment.length > 1
? segment.replace(/[\\:]/g, "\\$&")
: segment.replace(/\\/g, "\\\\")
);
}