Skip to content

Commit 0331ae9

Browse files
authored
Merge pull request ossec#2460 from atomicturtle/fix/ant-2026-nceh7rb6-json-rotate-flag
Forward the first JSON syslog alert after alerts.json rotates
2 parents c1bc60a + bff41c6 commit 0331ae9

5 files changed

Lines changed: 219 additions & 0 deletions

File tree

‎.github/workflows/make-multi-platform.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ jobs:
8787
./src/issue_q0uytpgc_udp_newline
8888
make -C src -f tests/regressions/Makefile issue_9q8lr4ny_syslog_placeholder
8989
./src/issue_9q8lr4ny_syslog_placeholder
90+
make -C src -f tests/regressions/Makefile issue_nceh7rb6_json_rotate_flag
91+
./src/issue_nceh7rb6_json_rotate_flag
9092
make -C src -f tests/regressions/Makefile issue_rxknzj1n_smtp_dot_escape
9193
./src/issue_rxknzj1n_smtp_dot_escape
9294
make -C src -f tests/regressions/Makefile issue_fbj3la25_rids_counter_reload

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Unrelease 4.5.0 changes
2929

3030
**Security Fixes**
3131

32+
- @atomicturtle - Forward the first JSON ``syslog_output`` alert written after ``alerts.json`` rotates or is truncated. ``jqueue_next()`` left ``JQUEUE_READ_FAILED`` set when the new file was still empty, and the next call parsed a good alert and deleted it. The alert stayed in ``alerts.json`` and still drove mail, the database, and active response (ANT-2026-NCEH7RB6)
3233
- @atomicturtle - Keep real ``syslog_output`` fields whose text happens to look like a placeholder. The default, CEF, and Splunk formatters dropped a value when a single character lined up with ``(none)``, ``(no user)``, or ``unknown``, so a user named ``anna``, a host ``inn.example.org``, or a location starting with ``(n`` never reached the receiver. Only the exact strings ``(none)`` and ``(no user)`` are left out. A user named ``unknown`` is still forwarded (ANT-2026-9Q8LR4NY)
3334
- @atomicturtle - Flatten CR and LF in every ``syslog_output`` alert, including UDP. TCP already replaced them with spaces so one alert stayed one frame; UDP sent the breaks, and a receiver that splits a datagram at newlines could store a line the manager never raised. UDP does not add a trailing newline. Receivers that relied on the line breaks inside a UDP ``default`` or ``splunk`` message, present since 3.4.0, now see spaces (ANT-2026-Q0UYTPGC)
3435
- @atomicturtle - Do not match a ``<field>`` rule when the decoder has no field table. ``OS_CheckIfRuleMatch()`` read ``fields[]`` for every such rule, and that table exists only for a decoder with ``<order>``, so one log line stopped ``ossec-analysisd``. A decoder that has the named field still matches (ANT-2026-R3ZY7CWM)

‎src/os_csyslogd/json-queue.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ cJSON *jqueue_parse_json(file_queue *queue)
177177
return NULL;
178178
}
179179

180+
/* This flag describes only the read about to happen. The rotation and
181+
* truncation paths return a failed read without clearing it, and the
182+
* next call would then delete a good alert. */
183+
queue->flags = 0;
184+
180185
initial_pos = ftell(queue->fp);
181186
if (initial_pos < 0) {
182187
queue->flags = JQUEUE_READ_FAILED;

‎src/tests/regressions/Makefile‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ REGRESSION_BINS = \
5151
issue_r3zy7cwm_field_null_decoder \
5252
issue_q0uytpgc_udp_newline \
5353
issue_9q8lr4ny_syslog_placeholder \
54+
issue_nceh7rb6_json_rotate_flag \
5455
multiline_underflow \
5556
win_network_path \
5657
checksum_short_message \
@@ -396,6 +397,18 @@ issue_r3zy7cwm_field_null_decoder: \
396397
-Wl,--start-group shared.a os_regex.a os_net.a os_xml.a os_zlib.a -Wl,--end-group \
397398
-lpcre2-8 -lm -lpthread -lz -lssl -lcrypto
398399

400+
# ANT-2026-NCEH7RB6: do not drop the first JSON alert after rotation.
401+
issue_nceh7rb6_json_rotate_flag: \
402+
tests/regressions/issue_nceh7rb6_json_rotate_flag.c \
403+
os_csyslogd/json-queue.c shared.a
404+
$(CC) $(CFLAGS) -I./os_csyslogd -I./external/cJSON \
405+
-c os_csyslogd/json-queue.c -o os_csyslogd/json-queue-nceh.o
406+
$(CC) $(CFLAGS) -I./os_csyslogd -I./external/cJSON -o $@ \
407+
tests/regressions/issue_nceh7rb6_json_rotate_flag.c \
408+
os_csyslogd/json-queue-nceh.o \
409+
-Wl,--start-group shared.a os_regex.a libcJSON.a -Wl,--end-group \
410+
-lpcre2-8 -lm -lpthread -lssl -lcrypto
411+
399412
# ANT-2026-9Q8LR4NY: do not drop a real syslog field that looks like anna.
400413
issue_9q8lr4ny_syslog_placeholder: \
401414
tests/regressions/issue_9q8lr4ny_syslog_placeholder.c \
@@ -1275,6 +1288,7 @@ clean:
12751288
analysisd/analysisd-r3z.o \
12761289
os_csyslogd/csyslog_send-q0u.o \
12771290
os_csyslogd/csyslogd-9q8.o \
1291+
os_csyslogd/json-queue-nceh.o \
12781292
addagent/validate-m5jt0jcl.o \
12791293
os_execd/exec-mon246f3.o \
12801294
os_execd/exec-mon246f3.c \
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Regression: stale JSON queue flag drops the first alert after rotation
3+
*
4+
* ANT-2026-NCEH7RB6 / CWE-372
5+
*
6+
* jqueue_next() reopens alerts.json when the inode changes or the file
7+
* shrinks, then returns that read directly. An empty new file sets
8+
* JQUEUE_READ_FAILED and the flag is not cleared. The next call parses a
9+
* real alert and deletes it because the flag is still set. The read
10+
* position has already moved past the record.
11+
*
12+
* Build (from src/):
13+
* make -f tests/regressions/Makefile issue_nceh7rb6_json_rotate_flag
14+
*
15+
* Run:
16+
* ./issue_nceh7rb6_json_rotate_flag
17+
*/
18+
19+
#include <stdio.h>
20+
#include <stdlib.h>
21+
#include <string.h>
22+
#include <unistd.h>
23+
24+
#include "shared.h"
25+
#include "cJSON.h"
26+
#include "json-queue.h"
27+
28+
const char *__local_name __attribute__((weak)) =
29+
"issue-nceh7rb6-json-rotate-flag";
30+
31+
static int failures = 0;
32+
33+
static void expect(int cond, const char *msg)
34+
{
35+
if (cond) {
36+
printf(" ok: %s\n", msg);
37+
} else {
38+
printf(" FAIL: %s\n", msg);
39+
failures++;
40+
}
41+
}
42+
43+
static int write_all(const char *path, const char *text)
44+
{
45+
FILE *fp = fopen(path, "w");
46+
47+
if (!fp) {
48+
return -1;
49+
}
50+
if (fputs(text, fp) == EOF) {
51+
fclose(fp);
52+
return -1;
53+
}
54+
return fclose(fp) == 0 ? 0 : -1;
55+
}
56+
57+
static int append_line(const char *path, const char *id)
58+
{
59+
FILE *fp = fopen(path, "a");
60+
int rc;
61+
62+
if (!fp) {
63+
return -1;
64+
}
65+
rc = fprintf(fp, "{\"id\":\"%s\"}\n", id);
66+
if (fclose(fp) != 0 || rc < 0) {
67+
return -1;
68+
}
69+
return 0;
70+
}
71+
72+
static const char *alert_id(cJSON *obj)
73+
{
74+
cJSON *id;
75+
76+
if (!obj) {
77+
return NULL;
78+
}
79+
id = cJSON_GetObjectItem(obj, "id");
80+
if (!id || id->type != cJSON_String || !id->valuestring) {
81+
return NULL;
82+
}
83+
return id->valuestring;
84+
}
85+
86+
static void take(file_queue *queue, const char *want, const char *msg)
87+
{
88+
cJSON *obj = jqueue_next(queue);
89+
const char *got = alert_id(obj);
90+
91+
expect(got != NULL && strcmp(got, want) == 0, msg);
92+
cJSON_Delete(obj);
93+
}
94+
95+
/* New empty alerts.json, then the first alert of the day. */
96+
static void test_inode_rotation(const char *dir)
97+
{
98+
char path[256];
99+
file_queue queue;
100+
101+
snprintf(path, sizeof(path), "%s/rotate.json", dir);
102+
expect(write_all(path, "{\"id\":\"before\"}\n") == 0, "write the old day's file");
103+
104+
jqueue_init(&queue);
105+
expect(jqueue_open_path(&queue, path, 0) == 0, "open the old day's file");
106+
take(&queue, "before", "alert before rotation is forwarded");
107+
108+
expect(unlink(path) == 0, "unlink the old day's file");
109+
expect(write_all(path, "") == 0, "new day's file is empty");
110+
expect(jqueue_next(&queue) == NULL, "empty new file yields nothing");
111+
112+
expect(append_line(path, "attacker") == 0, "first alert of the day is written");
113+
take(&queue, "attacker", "first alert after rotation is forwarded");
114+
115+
expect(append_line(path, "after") == 0, "later alert is written");
116+
take(&queue, "after", "alert after the first of the day is forwarded");
117+
jqueue_close(&queue);
118+
}
119+
120+
/* Alert already in the new file when the inode change is noticed. */
121+
static void test_alert_already_present(const char *dir)
122+
{
123+
char path[256];
124+
file_queue queue;
125+
126+
snprintf(path, sizeof(path), "%s/present.json", dir);
127+
expect(write_all(path, "{\"id\":\"before\"}\n") == 0, "write file before a filled rotation");
128+
129+
jqueue_init(&queue);
130+
expect(jqueue_open_path(&queue, path, 0) == 0, "open file before a filled rotation");
131+
take(&queue, "before", "pre-rotation alert is forwarded");
132+
133+
expect(unlink(path) == 0, "replace the file");
134+
expect(write_all(path, "{\"id\":\"already\"}\n") == 0, "new file already holds an alert");
135+
take(&queue, "already", "alert written before the reopen is forwarded");
136+
jqueue_close(&queue);
137+
}
138+
139+
/* Same inode, file truncated, then one new alert. */
140+
static void test_truncate(const char *dir)
141+
{
142+
char path[256];
143+
file_queue queue;
144+
FILE *fp;
145+
146+
snprintf(path, sizeof(path), "%s/trunc.json", dir);
147+
expect(write_all(path, "{\"id\":\"before\"}\n") == 0, "write file before truncate");
148+
149+
jqueue_init(&queue);
150+
expect(jqueue_open_path(&queue, path, 0) == 0, "open file before truncate");
151+
take(&queue, "before", "alert before truncate is forwarded");
152+
153+
fp = fopen(path, "w");
154+
expect(fp != NULL, "truncate the same inode");
155+
if (fp) {
156+
fclose(fp);
157+
}
158+
expect(jqueue_next(&queue) == NULL, "truncated empty file yields nothing");
159+
160+
expect(append_line(path, "attacker") == 0, "first alert after truncate is written");
161+
take(&queue, "attacker", "first alert after truncate is forwarded");
162+
jqueue_close(&queue);
163+
}
164+
165+
int main(void)
166+
{
167+
char dir[] = "/tmp/nceh7rb6-XXXXXX";
168+
169+
printf("JSON queue flag after alerts.json rotation\n");
170+
if (!mkdtemp(dir)) {
171+
perror("mkdtemp");
172+
return 1;
173+
}
174+
175+
test_inode_rotation(dir);
176+
test_alert_already_present(dir);
177+
test_truncate(dir);
178+
179+
{
180+
const char *names[] = {"rotate.json", "present.json", "trunc.json", NULL};
181+
char path[256];
182+
int i;
183+
184+
for (i = 0; names[i]; i++) {
185+
snprintf(path, sizeof(path), "%s/%s", dir, names[i]);
186+
unlink(path);
187+
}
188+
}
189+
rmdir(dir);
190+
191+
if (failures) {
192+
printf("FAIL: %d JSON rotation check(s)\n", failures);
193+
return 1;
194+
}
195+
printf("ok\n");
196+
return 0;
197+
}

0 commit comments

Comments
 (0)