Skip to content

Commit b005eda

Browse files
author
Commitfest Bot
committed
[CF 5845] Carefully exposing information without authentication
This branch was automatically generated by a robot using patches from an email thread registered at: https://commitfest.postgresql.org/patch/5845 The branch will be overwritten each time a new patch version is posted to the thread, and also periodically to check for bitrot caused by changes on the master branch. Patch(es): https://www.postgresql.org/message-id/CAKAnmmKxP7bOO7QOLdSk8dYoUxFRus2XC1nEbk6En9GgV_4JbA@mail.gmail.com Author(s): Greg Sabino Mullane
2 parents e8bfad4 + 01aed27 commit b005eda

File tree

7 files changed

+397
-0
lines changed

7 files changed

+397
-0
lines changed

doc/src/sgml/config.sgml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,84 @@ include_dir 'conf.d'
10771077
</variablelist>
10781078
</sect2>
10791079

1080+
<sect2 id="runtime-config-expose-settings">
1081+
<title>Expose Settings</title>
1082+
1083+
<variablelist>
1084+
1085+
<varlistentry id="guc-expose-recovery" xreflabel="expose_recovery">
1086+
<term><varname>expose_recovery</varname> (<type>boolean</type>)
1087+
<indexterm>
1088+
<primary><varname>expose_recovery</varname> configuration parameter</primary>
1089+
</indexterm>
1090+
</term>
1091+
<listitem>
1092+
<para>
1093+
Enables reporting if the server is in recovery mode without requiring
1094+
an authenticated login. Clients can send the string <literal>GET /replica</literal>
1095+
and will receive a 1 or 0. This is equivalent to logging in and running
1096+
<literal>SELECT pg_is_in_recovery()</literal>. A client can also send the
1097+
string <literal>HEAD /replica</literal> which will solely return an HTTP literal:
1098+
<literal>200</literal> if the server is in recovery, <literal>503</literal> if not.
1099+
(This allows a drop-in replacement to the same Patroni functionality)
1100+
Finally, a client can issue <literal>GET /info</literal> and receive the string
1101+
<literal>RECOVERY: </literal> followed by a 1 or 0.
1102+
</para>
1103+
</listitem>
1104+
</varlistentry>
1105+
1106+
<varlistentry id="guc-expose-sysid" xreflabel="expose_sysid">
1107+
<term><varname>expose_sysid</varname> (<type>boolean</type>)
1108+
<indexterm>
1109+
<primary><varname>expose_sysid</varname> configuration parameter</primary>
1110+
</indexterm>
1111+
</term>
1112+
<listitem>
1113+
<para>
1114+
Enables reporting the system identifier of the cluster without requiring
1115+
an authenticated login. Clients can send the string <literal>GET /sysid</literal>
1116+
and will receive the numeric system identifier. This is a unique number generated
1117+
by each cluster when initdb is run.
1118+
</para>
1119+
<para>
1120+
A client can issue <literal>GET /info</literal> and receive the string
1121+
<literal>SYSID: </literal> followed by the numeric system identifier.
1122+
</para>
1123+
<para>
1124+
This feature is useful for determining if the server is the same server as previously
1125+
encountered. Note than primary and replica servers will share the same system
1126+
identifier.
1127+
</para>
1128+
</listitem>
1129+
</varlistentry>
1130+
1131+
<varlistentry id="guc-expose-version" xreflabel="expose_version">
1132+
<term><varname>expose_version</varname> (<type>boolean</type>)
1133+
<indexterm>
1134+
<primary><varname>expose_version</varname> configuration parameter</primary>
1135+
</indexterm>
1136+
</term>
1137+
<listitem>
1138+
<para>
1139+
Enables reporting the numeric version of the Postgres cluster without requiring
1140+
an authenticated login. Clients can send the string <literal>GET /version</literal>
1141+
and will receive an integer representing the version.
1142+
</para>
1143+
<para>
1144+
A client can issue <literal>GET /info</literal> and receive the string
1145+
<literal>VERSION: </literal> followed by the numeric version.
1146+
</para>
1147+
<para>
1148+
This is particularly useful for non-Postgres systems (esp. security scanners) that
1149+
need a way to easily determine the version of Postgres in use without requiring
1150+
a Postgres client - or without needing any knowledge of the Postgres protocol at all.
1151+
</para>
1152+
</listitem>
1153+
</varlistentry>
1154+
1155+
</variablelist>
1156+
</sect2>
1157+
10801158
<sect2 id="runtime-config-connection-authentication">
10811159
<title>Authentication</title>
10821160

src/backend/tcop/backend_startup.c

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
bool Trace_connection_negotiation = false;
4747
uint32 log_connections = 0;
4848
char *log_connections_string = NULL;
49+
bool expose_recovery = false;
50+
bool expose_sysid = false;
51+
bool expose_version = false;
52+
4953

5054
/* Other globals */
5155

@@ -65,6 +69,7 @@ static void SendNegotiateProtocolVersion(List *unrecognized_protocol_options);
6569
static void process_startup_packet_die(SIGNAL_ARGS);
6670
static void StartupPacketTimeoutHandler(void);
6771
static bool validate_log_connections_options(List *elemlist, uint32 *flags);
72+
static bool ExposeInformation(pgsocket fd);
6873

6974
/*
7075
* Entry point for a new backend process.
@@ -148,6 +153,15 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
148153
StringInfoData ps_data;
149154
MemoryContext oldcontext;
150155

156+
/*
157+
* Scan for a simple GET / HEAD request. If this is detected and
158+
* handled, we are done and can immediately exit
159+
*/
160+
if ((expose_recovery || expose_sysid || expose_version)
161+
&& ExposeInformation(client_sock->sock))
162+
proc_exit(0);
163+
/* Should we do exit(0) here, despite the warnings in ipc.c? */
164+
151165
/* Tell fd.c about the long-lived FD associated with the client_sock */
152166
ReserveExternalFD();
153167

@@ -1126,3 +1140,180 @@ assign_log_connections(const char *newval, void *extra)
11261140
{
11271141
log_connections = *((int *) extra);
11281142
}
1143+
1144+
1145+
static
1146+
bool
1147+
ExposeInformation(pgsocket fd)
1148+
{
1149+
1150+
/*
1151+
* ExposeInformation
1152+
*
1153+
*
1154+
* Handle early socket probe before full backend startup.
1155+
* Responds to small set of predefined endpoints (e.g. GET /info)
1156+
*
1157+
* Requires at least one "expose_" GUC to be true.
1158+
*
1159+
* Returns true if any endpoint is recognized.
1160+
*/
1161+
1162+
#define EXPOSE_MIN_QUERY 9 /* Shortest possible line: "Get /info" */
1163+
#define EXPOSE_MAX_QUERY 16 /* Longest possible GET line */
1164+
1165+
/* What information is being returned */
1166+
typedef enum
1167+
{
1168+
EXPOSE_NOTHING,
1169+
EXPOSE_HEAD_REPLICA,
1170+
EXPOSE_GET_ALL,
1171+
EXPOSE_GET_REPLICA,
1172+
EXPOSE_GET_SYSID,
1173+
EXPOSE_GET_VERSION,
1174+
} ReturnType;
1175+
1176+
typedef struct
1177+
{
1178+
const char *endpoint;
1179+
const bool *require;
1180+
ReturnType type;
1181+
} endpoint_action;
1182+
1183+
static endpoint_action endpoint_actions[] =
1184+
{
1185+
{
1186+
"HEAD /replica", &expose_recovery, EXPOSE_HEAD_REPLICA
1187+
},
1188+
{
1189+
"GET /replica", &expose_recovery, EXPOSE_GET_REPLICA
1190+
},
1191+
{
1192+
"GET /sysid", &expose_sysid, EXPOSE_GET_SYSID
1193+
},
1194+
{
1195+
"GET /version", &expose_version, EXPOSE_GET_VERSION
1196+
},
1197+
{
1198+
"GET /info", NULL, EXPOSE_GET_ALL
1199+
}
1200+
};
1201+
1202+
ssize_t n;
1203+
char buf[EXPOSE_MAX_QUERY + 1];
1204+
int type;
1205+
1206+
Assert(expose_recovery || expose_sysid || expose_version);
1207+
1208+
do
1209+
{
1210+
n = recv(fd, buf, EXPOSE_MAX_QUERY, MSG_PEEK);
1211+
} while (n < 0 && errno == EINTR);
1212+
1213+
/*
1214+
* Leave as soon as possible if no chance we are interested. We also
1215+
* simply return false for n == -1
1216+
*/
1217+
if (n < EXPOSE_MIN_QUERY)
1218+
return false;
1219+
1220+
buf[n] = '\0';
1221+
1222+
type = EXPOSE_NOTHING;
1223+
for (int i = 0; i < lengthof(endpoint_actions); i++)
1224+
{
1225+
if (
1226+
strncmp(buf, endpoint_actions[i].endpoint, strlen(endpoint_actions[i].endpoint)) == 0
1227+
&&
1228+
(endpoint_actions[i].require == NULL
1229+
||
1230+
*(endpoint_actions[i].require)
1231+
))
1232+
{
1233+
type = endpoint_actions[i].type;
1234+
break;
1235+
}
1236+
}
1237+
1238+
if (type == EXPOSE_NOTHING)
1239+
return false;
1240+
1241+
{
1242+
static const char http_version[] = "HTTP/1.1";
1243+
static const char http_type[] = "Content-Type: text/plain";
1244+
static const char *http_conn = "Connection: close";
1245+
static const char http_len[] = "Content-Length";
1246+
1247+
StringInfoData msg;
1248+
1249+
if (type == EXPOSE_HEAD_REPLICA)
1250+
{
1251+
/*
1252+
* Caller only cares about the HTTP response code, so no content
1253+
* needed
1254+
*/
1255+
1256+
initStringInfoExt(&msg, 64);
1257+
1258+
appendStringInfo(&msg,
1259+
"%s %s\r\n"
1260+
"%s\r\n"
1261+
"%s\r\n\r\n",
1262+
http_version,
1263+
(RecoveryInProgress() ? "200 OK" : "503 Service Unavailable"),
1264+
http_type,
1265+
http_conn
1266+
);
1267+
}
1268+
else
1269+
{
1270+
StringInfoData content;
1271+
1272+
initStringInfoExt(&content, 64);
1273+
1274+
if (expose_recovery && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_REPLICA))
1275+
appendStringInfo(&content, "%s%d\r\n",
1276+
type == EXPOSE_GET_ALL ? "RECOVERY: " : "",
1277+
RecoveryInProgress() ? 1 : 0);
1278+
if (expose_sysid && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_SYSID))
1279+
appendStringInfo(&content, "%s%lu\r\n",
1280+
type == EXPOSE_GET_ALL ? "SYSID: " : "",
1281+
GetSystemIdentifier());
1282+
if (expose_version && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_VERSION))
1283+
appendStringInfo(&content, "%s%d\r\n",
1284+
type == EXPOSE_GET_ALL ? "VERSION: " : "",
1285+
PG_VERSION_NUM);
1286+
1287+
initStringInfoExt(&msg, 256);
1288+
1289+
appendStringInfo(&msg,
1290+
"%s 200 OK\r\n"
1291+
"%s\r\n"
1292+
"%s: %d\r\n"
1293+
"%s\r\n\r\n"
1294+
"%s",
1295+
http_version,
1296+
http_type,
1297+
http_len, content.len,
1298+
http_conn,
1299+
content.data
1300+
);
1301+
1302+
pfree(content.data);
1303+
}
1304+
1305+
do
1306+
{
1307+
n = send(fd, msg.data, msg.len, 0);
1308+
} while (n < 0 && errno == EINTR);
1309+
1310+
pfree(msg.data);
1311+
1312+
if (n < 0)
1313+
elog(DEBUG1, "could not send to client: %m");
1314+
1315+
return true;
1316+
1317+
}
1318+
1319+
}

src/backend/utils/misc/guc_parameters.dat

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@
106106
max => 'INT_MAX / 2',
107107
},
108108

109+
{ name => 'expose_recovery', type => 'bool', context => 'PGC_SIGHUP', group => 'CLIENT_CONN_STATEMENT',
110+
short_desc => 'Exposes if the server is in recovery mode without a login.',
111+
variable => 'expose_recovery',
112+
boot_val => 'false',
113+
},
114+
115+
{ name => 'expose_sysid', type => 'bool', context => 'PGC_SIGHUP', group => 'CLIENT_CONN_STATEMENT',
116+
short_desc => 'Exposes the system identifier without a login.',
117+
variable => 'expose_sysid',
118+
boot_val => 'false',
119+
},
120+
121+
{ name => 'expose_version', type => 'bool', context => 'PGC_SIGHUP', group => 'CLIENT_CONN_STATEMENT',
122+
short_desc => 'Exposes the server version without a login.',
123+
variable => 'expose_version',
124+
boot_val => 'false',
125+
},
126+
109127
{ name => 'array_nulls', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS',
110128
short_desc => 'Enables input of NULL elements in arrays.',
111129
long_desc => 'When turned on, unquoted NULL in an array input value means a null value; otherwise it is taken literally.',

src/backend/utils/misc/postgresql.conf.sample

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@
9191
# disconnection while running queries;
9292
# 0 for never
9393

94+
95+
# - Expose information -
96+
97+
#expose_recovery = off
98+
#expose_sysid = off
99+
#expose_version = off
100+
101+
94102
# - Authentication -
95103

96104
#authentication_timeout = 1min # 1s-600s

src/include/postmaster/postmaster.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ extern PGDLLIMPORT bool restart_after_crash;
7070
extern PGDLLIMPORT bool remove_temp_files_after_crash;
7171
extern PGDLLIMPORT bool send_abort_for_crash;
7272
extern PGDLLIMPORT bool send_abort_for_kill;
73+
extern PGDLLIMPORT bool expose_recovery;
74+
extern PGDLLIMPORT bool expose_sysid;
75+
extern PGDLLIMPORT bool expose_version;
76+
7377

7478
#ifdef WIN32
7579
extern PGDLLIMPORT HANDLE PostmasterHandle;

src/test/modules/test_misc/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ tests += {
1818
't/007_catcache_inval.pl',
1919
't/008_replslot_single_user.pl',
2020
't/009_log_temp_files.pl',
21+
't/010_expose.pl',
2122
],
2223
},
2324
}

0 commit comments

Comments
 (0)