From 8626b4c1d436659382d824d088dccff049e40714 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:25:15 +0200 Subject: [PATCH 1/4] httpd: don't let a quote in a port name break the web page A port named NAS"1 made status.json unparsable, so the page stopped loading, and the name survives a reboot in the saved config. VLAN names and the SFP strings have the same problem. They now go through a helper that replaces ", \ and non-printable bytes with a dot, the way the hostname is already sanitized on ingest. The length does not change, so the size checks still hold. The helper also covers the 0xa0 byte that send_sfp_info() skipped as a special case, so that check is gone. --- httpd/page_impl.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index cb11075c..1ed36c51 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -70,6 +70,14 @@ void char_to_html(char c) } +void json_char_to_html(uint8_t c) +{ + if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') + c = '.'; + outbuf[slen++] = c; +} + + // Convert uint8_t to ascii HEX char. void byte_to_html(uint8_t val) { @@ -198,8 +206,8 @@ void send_sfp_info(uint8_t sfp) if (i < 20 || i >= 60 || (i >= 36 && i < 40)) // Skip Non-ASCII codes continue; uint8_t c = sfp_buf[i & 0xf]; - if (c && c != 0xa0) // a0 is the byte read from a non-existant I2C EEPROM - char_to_html(c); + if (c) + json_char_to_html(c); } } @@ -293,7 +301,7 @@ void send_vlan(uint16_t vlan) dbg_string("VLAN has no name\n"); } else { while(vlan_names[n] && vlan_names[n] != ' ') - char_to_html(vlan_names[n++]); + json_char_to_html(vlan_names[n++]); } slen += strtox(outbuf + slen, "\",\"pvid\":\"0x"); uint16_t pvid_mask = 0; @@ -825,7 +833,7 @@ void send_status(void) itoa_html(i); slen += strtox(outbuf + slen, ",\"name\":\""); for (uint8_t j = 0; j < PORT_NAME_SIZE && port_names[i][j]; j++) { - char_to_html(port_names[i][j]); + json_char_to_html(port_names[i][j]); } slen += strtox(outbuf + slen, "\""); @@ -865,13 +873,13 @@ void send_status(void) } slen += strtox(outbuf + slen,"\",\"sfp_vendor\":\""); for (uint8_t s = 0; s < 16 && sfp_module_vendor[sfp][s]; s++) - outbuf[slen++] = sfp_module_vendor[sfp][s]; + json_char_to_html(sfp_module_vendor[sfp][s]); slen += strtox(outbuf + slen,"\",\"sfp_model\":\""); for (uint8_t s = 0; s < 16 && sfp_module_model[sfp][s]; s++) - outbuf[slen++] = sfp_module_model[sfp][s]; + json_char_to_html(sfp_module_model[sfp][s]); slen += strtox(outbuf + slen,"\",\"sfp_serial\":\""); for (uint8_t s = 0; s < 16 && sfp_module_serial[sfp][s]; s++) - outbuf[slen++] = sfp_module_serial[sfp][s]; + json_char_to_html(sfp_module_serial[sfp][s]); slen += strtox(outbuf + slen,"\",\"sfp_los\":"); if (machine.sfp_port[sfp].pin_los == GPIO_NA) { slen += strtox(outbuf + slen,"null"); @@ -1043,7 +1051,7 @@ void send_vlanlist(void) n = vlan_name(i); if (n != 0xffff) { while (vlan_names[n] && vlan_names[n] != ' ') - char_to_html(vlan_names[n++]); + json_char_to_html(vlan_names[n++]); } slen += strtox(outbuf + slen, "\"}"); From 56ee56af2e9b1908817ffaae6e7d7b1f97e46ad8 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Wed, 16 Sep 2026 23:05:11 +0200 Subject: [PATCH 2/4] cmd: keep quotes and control bytes out of names when they are set The console, POST /cmd and the saved config all go through the command parser, so one filter there covers every way a name can be set. The hostname already did this inline; port and VLAN names now do it too. The SFP strings still need escaping on the way out: they come from the module's EEPROM, not from a command. --- cmd_parser.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index d4c4df8b..ed973db8 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -440,6 +440,14 @@ static void cmd_error(__code const char *msg) // re-registers the static L2 management entry so the change applies without a // reboot. As a regular command a "mac ..." line in the startup config applies // on every boot (execute_config runs before the final static L2 entry). +static uint8_t name_char(uint8_t c) +{ + if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') + return '.'; + return c; +} + + void parse_mac_cmd(void) { if (cmd_words_len == 1) { @@ -627,7 +635,7 @@ void parse_vlan(void) *vlan_str++ = hex[(vlan_settings.vlan >> 4) & 0xf] ; *vlan_str++ = hex[vlan_settings.vlan & 0xf]; while(!cmd_is_space_or_nul(pos)) { - uint8_t c = cmd_buffer[pos++]; + uint8_t c = name_char(cmd_buffer[pos++]); write_char(c); *vlan_str++ = c; } @@ -889,7 +897,7 @@ void parse_port(void) } else if (cmd_compare(2, "name")) { uint8_t i = 0; while ( (i < PORT_NAME_SIZE-1) && (cmd_buffer[cmd_words_b[3] + i] != NUL) ) { - port_names[phy_settings.port][i] = cmd_buffer[cmd_words_b[3] + i]; + port_names[phy_settings.port][i] = name_char(cmd_buffer[cmd_words_b[3] + i]); i++; } port_names[phy_settings.port][i] = NUL; @@ -1831,9 +1839,7 @@ void cmd_parser(void) __banked uint8_t c = *hp++; if (c == NUL || c == '\r' || c == '\n') break; - if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') - c = '.'; - *dst++ = c; + *dst++ = name_char(c); } *dst = NUL; } else { From e2c4ed6be3b35cfb0235efd58cf3a20172d8c42e Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:10:22 +0200 Subject: [PATCH 3/4] sfp: clean the module strings where they are read The vendor, model and serial strings are filtered while the EEPROM read fills the buffers, so the page copies them out as they are. NUL still ends a string, so a field shorter than its slot reads the same as before. The three loops in send_status() become one call each. strcpy() cannot return the length: rtl837x_common.h hides that prototype under RTLP_HOST_TEST so host builds take the C library version, and a call that adds its result to slen does not compile there. A small static helper does the copy instead, and bank 1 drops by 96 bytes. --- httpd/page_impl.c | 16 ++++++++++------ sfp.c | 7 ++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 1ed36c51..58624145 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -70,6 +70,13 @@ void char_to_html(char c) } +static void str_x_to_html(__xdata const char *s) +{ + while (*s) + outbuf[slen++] = *s++; +} + + void json_char_to_html(uint8_t c) { if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') @@ -872,14 +879,11 @@ void send_status(void) sfp_send_data(sfp, 238, 1); } slen += strtox(outbuf + slen,"\",\"sfp_vendor\":\""); - for (uint8_t s = 0; s < 16 && sfp_module_vendor[sfp][s]; s++) - json_char_to_html(sfp_module_vendor[sfp][s]); + str_x_to_html(sfp_module_vendor[sfp]); slen += strtox(outbuf + slen,"\",\"sfp_model\":\""); - for (uint8_t s = 0; s < 16 && sfp_module_model[sfp][s]; s++) - json_char_to_html(sfp_module_model[sfp][s]); + str_x_to_html(sfp_module_model[sfp]); slen += strtox(outbuf + slen,"\",\"sfp_serial\":\""); - for (uint8_t s = 0; s < 16 && sfp_module_serial[sfp][s]; s++) - json_char_to_html(sfp_module_serial[sfp][s]); + str_x_to_html(sfp_module_serial[sfp]); slen += strtox(outbuf + slen,"\",\"sfp_los\":"); if (machine.sfp_port[sfp].pin_los == GPIO_NA) { slen += strtox(outbuf + slen,"null"); diff --git a/sfp.c b/sfp.c index 7bc4f301..1f1f6652 100644 --- a/sfp.c +++ b/sfp.c @@ -88,7 +88,12 @@ bool sfp_read_field(__xdata char *dst, uint8_t sfp, uint8_t start, uint8_t lengt return false; dst[length] = NUL; - memcpy(dst, sfp_buf, length); + for (uint8_t i = 0; i < length; i++) { + uint8_t c = sfp_buf[i]; + if (c && (c < 0x20 || c > 0x7e || c == '"' || c == '\\')) + c = '.'; + dst[i] = c; + } while (length > 0 && dst[--length] == ' ') dst[length] = NUL; From a7fc8303e9e26c17a248128f02f4c234dd63c16b Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:32:54 +0200 Subject: [PATCH 4/4] cmd: move name_char() above the parse_mac_cmd comment The helper landed between that comment and the function it describes, so the comment now reads as if it belonged to name_char(). Moving the helper up puts the two back together. The generated code is unchanged. --- cmd_parser.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index ed973db8..e5032024 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -434,12 +434,6 @@ static void cmd_error(__code const char *msg) } -// Sets the management MAC from a "mac " command (or prints -// the current one with a bare "mac"). Refuses blank/multicast/locally -// administered/all-zero-OUI addresses so the running MAC stays reachable, and -// re-registers the static L2 management entry so the change applies without a -// reboot. As a regular command a "mac ..." line in the startup config applies -// on every boot (execute_config runs before the final static L2 entry). static uint8_t name_char(uint8_t c) { if (c < 0x20 || c > 0x7e || c == '"' || c == '\\') @@ -448,6 +442,12 @@ static uint8_t name_char(uint8_t c) } +// Sets the management MAC from a "mac " command (or prints +// the current one with a bare "mac"). Refuses blank/multicast/locally +// administered/all-zero-OUI addresses so the running MAC stays reachable, and +// re-registers the static L2 management entry so the change applies without a +// reboot. As a regular command a "mac ..." line in the startup config applies +// on every boot (execute_config runs before the final static L2 entry). void parse_mac_cmd(void) { if (cmd_words_len == 1) {