Skip to content

Commit eedeeac

Browse files
authored
fix: LISTEN with special characters in channel names (#119)
* Add test for `LISTEN` channel name containing special characters Provides reproducible example for issue #118: A channel containing special characters generates invalid SQL due to the pgo not escaping such identifiers. * Escape channel names in calls to `pgo_notifications:listen` By escaping channel names on the way to the database, the `listen` function is now able to function with special characters in channel names, as well as being safe from SQL injection. The names are NOT quoted when stored in the in-memory listener map, so that the raw channel names match with the names reported by the database when the notification occurs. Closes #118
1 parent 9ad067b commit eedeeac

File tree

2 files changed

+28
-4
lines changed

2 files changed

+28
-4
lines changed

src/pgo_notifications.erl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,15 @@ disconnected({call, {Pid, _}=From}, {listen, Channel}, Data=#data{listeners=List
9797
disconnected(EventType, EventContent, Data) ->
9898
handle_event(EventType, EventContent, Data).
9999

100+
escape_channel_name(Name) ->
101+
["\"", string:replace(Name, "\"", "\"\"", all), "\""].
102+
100103
connected(enter, _, #data{conn=Conn,
101104
listener_channels=ListenerChannels}) ->
102105
case map_size(ListenerChannels) > 0 of
103106
true ->
104107
Channels = maps:keys(ListenerChannels),
105-
ListenStatements = [["LISTEN ", Channel, "; "] || Channel <- Channels],
108+
ListenStatements = [["LISTEN ", escape_channel_name(Channel), "; "] || Channel <- Channels],
106109
Query = ["DO $$BEGIN ", ListenStatements, " END$$"],
107110
#{command := do} = pgo_handler:extended_query(Conn, Query, []),
108111
keep_state_and_data;
@@ -120,7 +123,7 @@ connected({call, {Pid, _}=From}, {listen, Channel}, Data=#data{conn=Conn,
120123
case erlang:map_size(maps:get(Channel, ListenerChannels1)) of
121124
1 ->
122125
%% first time listening on this channel
123-
case pgo_handler:extended_query(Conn, ["LISTEN ", Channel], []) of
126+
case pgo_handler:extended_query(Conn, ["LISTEN ", escape_channel_name(Channel)], []) of
124127
#{command := listen} ->
125128
{keep_state, Data#data{listeners=Listeners1,
126129
listener_channels=ListenerChannels1}, [{reply, From, {ok, Ref}}]};
@@ -209,7 +212,7 @@ unlisten(Ref, Listeners, ListenerChannels, Conn) ->
209212
case erlang:map_size(PerChannel) of
210213
0 ->
211214
ListenerChannels1 =maps:remove(Channel, ListenerChannels),
212-
#{command := unlisten} = pgo_handler:extended_query(Conn, ["UNLISTEN ", Channel], []),
215+
#{command := unlisten} = pgo_handler:extended_query(Conn, ["UNLISTEN ", escape_channel_name(Channel)], []),
213216
{Listeners1, ListenerChannels1};
214217
_ ->
215218
{Listeners1, ListenerChannels#{Channel => PerChannel}}

test/pgo_listen_SUITE.erl

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
-include_lib("common_test/include/ct.hrl").
66
-include_lib("stdlib/include/assert.hrl").
77

8-
all() -> [listen_notify, named_listen_notify].
8+
all() -> [listen_notify, named_listen_notify, listen_notify_special_name].
99

1010
init_per_suite(Config) ->
1111
application:ensure_all_started(pgo),
@@ -126,3 +126,24 @@ named_listen_notify(_Config) ->
126126
end,
127127

128128
ok.
129+
130+
listen_notify_special_name(_Config) ->
131+
{ok, Pid} = pgo_notifications:start_link(#{database => "test",
132+
user => "test",
133+
password => "password"}),
134+
Chan1 = <<"chan1:\"hello\"">>,
135+
136+
{_, Ref1} = pgo_notifications:listen(Pid, Chan1),
137+
138+
?assertMatch(#{command := notify},
139+
pgo:query("NOTIFY \"chan1:\"\"hello\"\"\", 'message 1'")),
140+
141+
receive
142+
{notification, Pid, Ref1, Chan1, Payload} ->
143+
?assertEqual(<<"message 1">>, Payload)
144+
after
145+
1000 ->
146+
ct:fail(timeout)
147+
end,
148+
149+
ok.

0 commit comments

Comments
 (0)