From 82cb00243f63fe4ded08f0e7eaf1c3d588536165 Mon Sep 17 00:00:00 2001 From: "Rondineli G. Saad" Date: Wed, 23 Sep 2026 10:53:09 -0300 Subject: [PATCH 1/2] fix(security): endurece servicos e migracao iAH --- docker/rocky9/httpd-scielo.conf | 11 +- docker/rocky9/scielo-instance-check.sh | 62 ++++-- docs/rocky9-instance-migration-checklist.md | 10 +- .../vulnerability-assessment-2026-09.md | 81 +++++-- .../Open_Flash_Chart/ofc-library/Graph.php | 11 +- .../classes/domit-1/xml_domit_doctor.php | 4 +- .../domit-1/xml_domit_getelementsbypath.php | 22 +- .../classes/domit-1/xml_domit_lite_parser.php | 61 ++++-- .../classes/domit-1/xml_domit_shared.php | 16 +- .../classes/domit-1/xml_domit_utilities.php | 26 +-- .../classes/domit-1/xml_saxy_lite_parser.php | 12 +- .../classes/domit-1/xml_saxy_shared.php | 10 +- .../classes/services/AccessServiceBar.php | 48 +++-- .../scielo-org/classes/services/Service.php | 7 +- .../pages/services/articleRequestGraphic.php | 32 ++- .../services/articleRequestGraphicPage.php | 100 +++++---- .../scielo-org/pages/services/sendMail.php | 204 ++++++++++-------- htdocs/security.php | 148 +++++++++++++ htdocs/statjournal.php | 81 ++++--- tests/security_regression.php | 45 ++++ 20 files changed, 708 insertions(+), 283 deletions(-) diff --git a/docker/rocky9/httpd-scielo.conf b/docker/rocky9/httpd-scielo.conf index 8ca4341e4..234ba898c 100644 --- a/docker/rocky9/httpd-scielo.conf +++ b/docker/rocky9/httpd-scielo.conf @@ -16,6 +16,10 @@ Alias /pdf/ "/var/www/html/bases/pdf/" Require all denied + + + Require all denied + @@ -27,7 +31,12 @@ Alias /pdf/ "/var/www/html/bases/pdf/" Options -Indexes +ExecCGI +FollowSymLinks AllowOverride None - Require all granted + AddDefaultCharset ISO-8859-1 + Require all denied + + + Require all granted + diff --git a/docker/rocky9/scielo-instance-check.sh b/docker/rocky9/scielo-instance-check.sh index ed7266a5b..b50c7b0a6 100755 --- a/docker/rocky9/scielo-instance-check.sh +++ b/docker/rocky9/scielo-instance-check.sh @@ -216,16 +216,17 @@ apply_index() { backup_iy0 "$db" } -reindex_with_fst() { - local db="$1" - local fst="$2" - local label="$3" +reindex_to_target_with_fst() { + local source_db="$1" + local target_db="$2" + local fst="$3" + local label="$4" local safe_label safe_label="$(sed 's#[^A-Za-z0-9_.-]#_#g' <<<"$label")" local tmpbase="$TMPDIR/$safe_label" - if ! has_database "$db"; then - warn "$label skipped; database files missing: $db.mst/.xrf" + if ! has_database "$source_db"; then + warn "$label skipped; source database files missing: $source_db.mst/.xrf" return fi if [[ ! -f "$fst" ]]; then @@ -235,18 +236,30 @@ reindex_with_fst() { if [[ "$MODE" == "fix" ]]; then mkdir -p "$TMPDIR" - "$MX" "$db" "fst=@$fst" "fullinv=$tmpbase" -all now >/dev/null - apply_index "$tmpbase" "$db" + ( + cd "$ROOT/proc" + "$MX" "$source_db" "fst=@$fst" \ + "actab=tabs/acans.tab" "uctab=tabs/ucans.tab" \ + "fullinv=$tmpbase" -all now >/dev/null + ) + apply_index "$tmpbase" "$target_db" ok "$label reindexed from $fst" else - if [[ -f "$db.iy0" ]]; then - warn "$label has legacy .iy0 index: $db.iy0" + if [[ -f "$target_db.iy0" ]]; then + warn "$label has legacy .iy0 index: $target_db.iy0" else ok "$label has modern index files or no legacy .iy0" fi fi } +reindex_with_fst() { + local db="$1" + local fst="$2" + local label="$3" + reindex_to_target_with_fst "$db" "$db" "$fst" "$label" +} + reindex_pid880() { local db="$1" local label="$2" @@ -286,20 +299,35 @@ check_indexes() { reindex_with_fst "$ROOT/bases/title/logo" "$ROOT/proc/fst/logo.fst" "logo" reindex_with_fst "$ROOT/bases/newissue/newissue" "$ROOT/proc/fst/newissue.fst" "newissue" reindex_with_fst "$ROOT/bases/artigo/artigo" "$ROOT/proc/fst/artigo.fst" "artigo" + reindex_to_target_with_fst "$ROOT/bases/artigo/artigo" "$ROOT/bases/artigo/author" \ + "$ROOT/proc/fst/author.fst" "author" reindex_with_fst "$ROOT/bases/issue/issue" "$ROOT/proc/fst/issue.fst" "issue" reindex_with_fst "$ROOT/bases/issue/facic" "$ROOT/proc/fst/facic.fst" "facic" reindex_with_fst "$ROOT/bases/issue/faccount" "$ROOT/proc/fst/faccount.fst" "faccount" reindex_pid880 "$ROOT/bases/cited/cited" "cited" reindex_pid880 "$ROOT/bases/related/related" "related" - local search_db - while IFS= read -r search_db; do - reindex_with_fst "${search_db%.mst}" "$ROOT/proc/fst/search.fst" "iah-search-${search_db%.mst}" - done < <(find "$ROOT/bases/iah" -path '*/search.mst' -type f 2>/dev/null | sort) + local iah_dir collection source_db search_db searchp_db + while IFS= read -r iah_dir; do + collection="$(basename "$iah_dir")" + if [[ "$collection" == "library" ]]; then + source_db="$ROOT/bases/artigo/artigo" + else + source_db="$ROOT/bases/$collection/$collection" + fi - while IFS= read -r search_db; do - reindex_with_fst "${search_db%.mst}" "$ROOT/proc/fst/searchp.fst" "iah-searchp-${search_db%.mst}" - done < <(find "$ROOT/bases/iah" -path '*/searchp.mst' -type f 2>/dev/null | sort) + search_db="$iah_dir/search" + if [[ -f "$search_db.iy0" || -f "$search_db.cnt" ]]; then + reindex_to_target_with_fst "$source_db" "$search_db" \ + "$ROOT/proc/fst/search.fst" "iah-search-$collection" + fi + + searchp_db="$iah_dir/searchp" + if [[ -f "$searchp_db.iy0" || -f "$searchp_db.cnt" ]]; then + reindex_to_target_with_fst "$source_db" "$searchp_db" \ + "$ROOT/proc/fst/searchp.fst" "iah-searchp-$collection" + fi + done < <(find "$ROOT/bases/iah" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort) } check_remaining_iy0() { diff --git a/docs/rocky9-instance-migration-checklist.md b/docs/rocky9-instance-migration-checklist.md index f48214b22..7176721ac 100644 --- a/docs/rocky9-instance-migration-checklist.md +++ b/docs/rocky9-instance-migration-checklist.md @@ -64,7 +64,10 @@ Run this only after confirming the mounted directory is the instance you want to docker compose exec -T scielo-web scielo-instance-check.sh --root /var/www/html --fix ``` -The repair mode rebuilds known ISIS indexes and moves active `.iy0` files to timestamped backups such as `title.iy0.bak.YYYYMMDDHHMMSS`. +The repair mode rebuilds known ISIS indexes and moves active `.iy0` files to +timestamped backups such as `title.iy0.bak.YYYYMMDDHHMMSS`. Derived iAH indexes +that do not have their own `.mst/.xrf` (`author`, `search` and `searchp`) are rebuilt +from the corresponding article database. ## Optional container startup mode @@ -110,5 +113,8 @@ only when the instance file does not exist. ## Notes - The image can be healthy while a mounted legacy instance is not. Treat each instance as data that needs a compatibility pass. -- The most common failure after moving to Rocky9/PHP 8 is `WXIS|fatal error|unavoidable|leafread/ock|`, usually caused by legacy `.iy0` files. +- The most common failure after moving to Rocky9/PHP 8 is + `WXIS|fatal error|unavoidable|leafread/ock|`, usually caused by legacy `.iy0` + files. Browsers may present this response as a download because WXIS emits + `application/octet-stream` for the fatal error. - Rebuilding indexes does not rewrite `.mst` or `.xrf` data files. diff --git a/docs/security/vulnerability-assessment-2026-09.md b/docs/security/vulnerability-assessment-2026-09.md index e98dfc909..b1a5938cb 100644 --- a/docs/security/vulnerability-assessment-2026-09.md +++ b/docs/security/vulnerability-assessment-2026-09.md @@ -5,9 +5,10 @@ Este documento confronta os 11 achados do relatorio `Res_Seg_Scielo_09_2026.pdf` com o codigo presente na branch `codex/security-remediation-sep-2026`. -A avaliacao e estatica e nao destrutiva. Nenhuma prova de conceito foi executada -contra ambientes externos. As evidencias de execucao apresentadas no relatorio -foram consideradas evidencias do auditor, e nao foram repetidas. +A avaliacao combina revisao estatica com testes locais nao destrutivos. Nenhuma prova +de conceito foi executada contra ambientes externos. As evidencias de execucao +apresentadas no relatorio foram consideradas evidencias do auditor e nao foram +repetidas fora do ambiente local isolado. Foram revisados 17 arquivos de implementacao, configuracao e artefatos diretamente relacionados aos achados. Tambem foi feita uma varredura de padroes de segredo nos @@ -54,6 +55,21 @@ considerados encerrados: - Eventos negados e falhas de backend sao registrados em JSON sem payloads, e-mails, cookies, tokens ou valores de configuracao. +Em 22 de setembro de 2026, a primeira rodada P1 tambem foi implementada: + +- 004: `statjournal.php` valida idioma, ISSN e colecao; parametros dos links de + analytics sao codificados e marcadores fora da allowlist retornam HTTP 400. +- 007: o Apache bloqueia arquivos `.inc`, `.def`, `.template`, arquivos de versao e + README no DocumentRoot. +- 010: os dois endpoints do grafico validam PID e anos, codificam HTML por contexto e + usam `json_encode` para valores inseridos em JavaScript. +- 011: `sendMail.php` exige POST, token CSRF, validacao de e-mails, nomes e comentario, + limita cinco envios por hora/IP e registra somente resultado, motivo e PID. +- 006 e 008: `cgi-bin` passou a negar tudo por padrao e permitir apenas `wxis.exe`; + `mimetex.cgi` e `temp/wxis` retornam HTTP 403. A remocao fisica de `mimetex.cgi` e o + bloqueio externo completo do WXIS permanecem pendentes para evitar regressao nos + consumidores legados que ainda usam o hostname publico. + Validacoes executadas: - lint PHP 8.5 em 15 arquivos: aprovado; @@ -63,12 +79,45 @@ Validacoes executadas: - `debug` e `diag` externos nao exibiram marcadores de diagnostico; - `debug=xml` via loopback retornou HTTP 200 e `text/plain`; - `git diff --check`: aprovado. - -Os endpoints `articleRequestGraphicPage.php` e `sendMail.php` ainda retornam HTTP 500 -em PHP 8.5 tanto no caso de controle quanto no caso com `caller` adulterado. O log -atribui a falha preexistente a uma chamada estatica de metodo nao estatico na biblioteca -DOMIT. A resposta confirma que o backend usado foi o host configurado, mas a correcao -de compatibilidade DOMIT deve preceder o reteste funcional completo desses dois fluxos. +- lint PHP 8.5 da rodada P1: aprovado; +- sintaxe Apache (`httpd -t`): aprovada; +- arquivos auxiliares, `mimetex.cgi` e `temp/wxis`: HTTP 403; +- entrada XSS nos endpoints de estatisticas: HTTP 400; +- envio sem token CSRF: HTTP 403 com auditoria sem campos do formulario; +- `wxis.exe`: HTTP 200 por permanecer explicitamente na allowlist de compatibilidade. +- parser DOMIT validado com XML real no PHP 8.5; +- paginas de estatisticas e formulario de e-mail retornaram HTTP 200 com `caller` + adulterado, sem refletir o dominio recebido; +- formulario de e-mail apresentou token CSRF de 256 bits e rejeitou entrada invalida + com token valido usando HTTP 400, sem acionar o transporte SMTP. + +Em 23 de setembro de 2026, a branch tambem foi testada com a copia da instancia +`cienciaecultura_bvs_br`. Codigo, bases, CGI e procedimentos foram montados como +somente leitura; uma copia temporaria da configuracao teve credenciais removidas e +destinos de rede direcionados para loopback. Com um PID existente na base: + +- pagina inicial, artigo, WXIS, exportacao XML, RSS, estatisticas do periodico, + formulario de e-mail e pagina/dados do grafico responderam HTTP 200; +- arquivos de configuracao, templates, README, versao e CGI fora da allowlist + responderam HTTP 403; +- `caller` adulterado nao foi refletido nem usado como backend; +- diagnostico XML continuou disponivel em loopback, mas foi negado para um cliente + em outro contêiner e gerou evento de auditoria; +- POST de e-mail sem CSRF respondeu HTTP 403; sessao e token validos combinados com + e-mail invalido responderam HTTP 400, sem tentativa SMTP; +- o teste revelou e permitiu corrigir dois erros PHP 8.5 no grafico: ausencia de + estatisticas em determinados anos e construtor legado da classe `Graph`. + +Os logs ainda registram avisos/depreciacoes fora do escopo imediato, principalmente +propriedades dinamicas em `class.XSLTransformer.php` e chaves opcionais ausentes na +configuracao de estatisticas. Esses pontos entram no backlog P2 de compatibilidade. + +A incompatibilidade observada na biblioteca DOMIT foi corrigida de forma localizada: +metodos utilitarios chamados estaticamente foram declarados como estaticos, classes +usadas pelo parser receberam adaptadores de construtor para PHP 8 e a insercao duplicada +do elemento raiz foi removida. O envio real por SMTP nao foi executado localmente para +evitar efeitos externos e ainda deve ser validado com caixa postal sintetica em +homologacao. ## Matriz de validacao e risco @@ -77,15 +126,15 @@ de compatibilidade DOMIT deve preceder o reteste funcional completo desses dois | 001 | Mitigado na branch; aguarda homologacao | O modo debug publico foi bloqueado; XML de compatibilidade ficou restrito a loopback e todos os diagnosticos usam `text/plain` | CWE-79, CWE-489 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N` | P0 | | 002 | Mitigado na branch; aguarda homologacao | `old2new.inc` nao exporta mais chaves arbitrarias para `$GLOBALS`; `classRequestVars.php` usa superglobais explicitas | CWE-621, CWE-915 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N` | P0 | | 003 | Mitigado na branch; aguarda homologacao | `export.php` usa exclusivamente `SERVER_SCIELO` validado; variantes em RSS e paginas SciELO Org receberam a mesma protecao | CWE-918 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N` | P0 | -| 004 | Confirmado | `statjournal.php:33-39` nao altera `$_GET`; `statjournal.php:186-220` insere `collection` em atributos `href` sem escape | CWE-79 | 5.1 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N` | P1 | +| 004 | Mitigado na branch; aguarda homologacao | Idioma, ISSN e colecao usam allowlist; os valores de query sao codificados e entradas com markup retornam HTTP 400 | CWE-79 | 5.1 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N` | P1 | | 005 | Mitigado na branch; aguarda homologacao | `diag` exige habilitacao explicita e loopback; tentativas externas seguem o fluxo normal e geram auditoria sem dados sensiveis | CWE-200, CWE-489 | 6.9 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P0 | -| 006 | Confirmado | `httpd-scielo.conf:27-30` concede acesso publico e execucao a todo o diretorio `cgi-bin`; nao existe regra especifica para WXIS | CWE-200, CWE-284 | 6.9 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P1 | -| 007 | Confirmado | `httpd-scielo.conf:11-19` publica todo o DocumentRoot e bloqueia somente `phpinfo.php`/`info.php`; os arquivos citados ficam dentro de `htdocs` | CWE-552 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P1 | -| 008 | Condicional | `mimetex.cgi` existe e `httpd-scielo.conf:27-30` permite CGI; o arquivo e um executavel PE32 para Windows e tende a falhar no Rocky Linux | CWE-1104 | N/A ate identificar versao/CVE e ambiente em que o binario pode executar | P1 - remover | +| 006 | Parcialmente mitigado | `cgi-bin` nega acesso por padrao e permite somente `wxis.exe`; o acesso externo ao WXIS continua habilitado para compatibilidade com consumidores legados | CWE-200, CWE-284 | 6.9 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P1 | +| 007 | Mitigado na branch; aguarda homologacao | Apache retorna HTTP 403 para `.inc`, `.def`, `.template`, README e arquivos de versao, mantendo estaticos comuns acessiveis | CWE-552 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N` | P1 | +| 008 | Parcialmente mitigado | `mimetex.cgi` retorna HTTP 403 pela politica default-deny de CGI, mas o binario ainda deve ser removido do repositorio e do artefato | CWE-1104 | N/A ate identificar versao/CVE e ambiente em que o binario pode executar | P1 - remover | | 009 | Parcialmente confirmado | `scielolog.php`, `scielologofi.php` e `sci_statart.php` existem e dependem de globais legadas; o HTTP 500 nao foi reproduzido localmente | CWE-1050 | N/A - superficie legada sem impacto exploravel demonstrado | P2 | -| 010 | Confirmado | `articleRequestGraphicPage.php:17-33,44,109-123` reflete `pid`, `caller`, `lang` e anos em HTML/JavaScript sem codificacao contextual | CWE-79 | 5.1 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N` | P1 | -| 011 | Condicional | `sendMail.php:24-36,41-47,80-104` aceita destinatario, Reply-To, nomes e comentario sem autenticacao, rate limit ou validacao no servidor | CWE-400, CWE-799, CWE-20 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:L/SI:L/SA:L` | P1 se SMTP ativo; P2 se desativado | -| A-01 | Mitigado na branch; reteste funcional bloqueado por DOMIT | Os endpoints ignoram `caller` como host e usam `SERVER_SCIELO`; testes confirmaram o destino configurado antes da falha preexistente da biblioteca DOMIT | CWE-918 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N` | P0 | +| 010 | Mitigado na branch; aguarda homologacao | PID, idioma e anos sao validados; HTML usa escape contextual e JavaScript recebe valores por `json_encode`; pagina retornou HTTP 200 no PHP 8.5 | CWE-79 | 5.1 Media - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N` | P1 | +| 011 | Mitigado na branch; aguarda teste SMTP em homologacao | O envio exige POST e CSRF, valida e limita campos, aplica cinco tentativas por hora/IP e nao retorna detalhes do PHPMailer | CWE-400, CWE-799, CWE-20 | 6.3 Media - `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:L/SI:L/SA:L` | P1 se SMTP ativo; P2 se desativado | +| A-01 | Mitigado na branch; aguarda homologacao | Os endpoints ignoram `caller` como host, usam `SERVER_SCIELO` e retornaram HTTP 200 no PHP 8.5 com dominio adulterado | CWE-918 | 8.8 Alta - `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:L/SC:N/SI:N/SA:N` | P0 | ### Observacoes sobre as severidades diff --git a/htdocs/applications/scielo-org/classes/Open_Flash_Chart/ofc-library/Graph.php b/htdocs/applications/scielo-org/classes/Open_Flash_Chart/ofc-library/Graph.php index af9bfacb9..e1b3ea0ba 100644 --- a/htdocs/applications/scielo-org/classes/Open_Flash_Chart/ofc-library/Graph.php +++ b/htdocs/applications/scielo-org/classes/Open_Flash_Chart/ofc-library/Graph.php @@ -1,7 +1,12 @@ Graph(); + } + + function Graph() { $this->data = array(); $this->x_labels = array(); diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_doctor.php b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_doctor.php index 4fe9327a4..c7f61f542 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_doctor.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_doctor.php @@ -22,7 +22,7 @@ class domit_doctor { * @param string The xml text to be repaired * @return string The repaired xml text */ - function fixAmpersands($xmlText) { + public static function fixAmpersands($xmlText) { $xmlText = trim($xmlText); $startIndex = -1; $processing = true; @@ -91,4 +91,4 @@ function evaluateCharacter($xmlText, $illegalChar, $startIndex) { return $xmlText; } //evaluateCharacter } //domit_doctor -?> \ No newline at end of file +?> diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_getelementsbypath.php b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_getelementsbypath.php index 0472c3cec..30fcccfaf 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_getelementsbypath.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_getelementsbypath.php @@ -43,10 +43,14 @@ class DOMIT_GetElementsByPath { /** * Constructor - creates an empty DOMIT_NodeList to store matching nodes */ - function DOMIT_GetElementsByPath() { + function DOMIT_GetElementsByPath() { require_once(DOMIT_INCLUDE_PATH . 'xml_domit_nodemaps.php'); - $this->nodeList = new DOMIT_NodeList(); - } //DOMIT_GetElementsByPath + $this->nodeList = new DOMIT_NodeList(); + } //DOMIT_GetElementsByPath + + function __construct() { + $this->DOMIT_GetElementsByPath(); + } /** * Parses the supplied "path"-based pattern @@ -219,10 +223,14 @@ class DOMIT_GetElementsByAttributePath { /** * Constructor - creates an empty DOMIT_NodeList to store matching nodes */ - function DOMIT_GetElementsByAttributePath() { + function DOMIT_GetElementsByAttributePath() { require_once(DOMIT_INCLUDE_PATH . 'xml_domit_nodemaps.php'); - $this->nodeList = new DOMIT_NodeList(); - } //DOMIT_GetElementsByAttributePath + $this->nodeList = new DOMIT_NodeList(); + } //DOMIT_GetElementsByAttributePath + + function __construct() { + $this->DOMIT_GetElementsByAttributePath(); + } /** * Matches the current path segment against the child nodes of the current context node @@ -274,4 +282,4 @@ function &parsePattern(&$node, $pattern, $nodeIndex = 0) { } } //parsePattern } //DOMIT_GetElementsByAttributePath -?> \ No newline at end of file +?> diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_lite_parser.php b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_lite_parser.php index 25bf8d828..396425977 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_lite_parser.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_lite_parser.php @@ -643,7 +643,7 @@ class DOMIT_Lite_Document extends DOMIT_Lite_ChildNodes_Interface { /** * DOM Document constructor */ - function DOMIT_Lite_Document() { + function DOMIT_Lite_Document() { $this->_constructor(); $this->xmlDeclaration = ''; $this->doctype = ''; @@ -652,8 +652,12 @@ function DOMIT_Lite_Document() { $this->nodeName = '#document'; $this->ownerDocument =& $this; $this->parser = ''; - $this->implementation = new DOMIT_DOMImplementation(); - } //DOMIT_Lite_Document + $this->implementation = new DOMIT_DOMImplementation(); + } //DOMIT_Lite_Document + + function __construct() { + $this->DOMIT_Lite_Document(); + } /** * Specifies whether DOMIT! Lite will try to fix invalid XML before parsing begins @@ -769,11 +773,10 @@ function &setDocumentElement(&$node) { * @param Object The node to be appended * @return Object The appended node */ - function &appendChild(&$node) { - if ($node->nodeType == DOMIT_ELEMENT_NODE) { - if ($this->documentElement == null) { - parent::appendChild($node); - $this->setDocumentElement($node); + function &appendChild(&$node) { + if ($node->nodeType == DOMIT_ELEMENT_NODE) { + if ($this->documentElement == null) { + $this->setDocumentElement($node); } else { //error thrown if documentElement already exists! @@ -1241,13 +1244,17 @@ class DOMIT_Lite_Element extends DOMIT_Lite_ChildNodes_Interface { * DOM Element constructor * @param string The tag name of the element */ - function DOMIT_Lite_Element($tagName) { + function DOMIT_Lite_Element($tagName) { $this->_constructor(); $this->nodeType = DOMIT_ELEMENT_NODE; $this->nodeName = $tagName; $this->attributes = array(); - $this->childNodes = array(); - } //DOMIT_Lite_Element + $this->childNodes = array(); + } //DOMIT_Lite_Element + + function __construct($tagName) { + $this->DOMIT_Lite_Element($tagName); + } /** * Returns the tag name of the element @@ -1517,12 +1524,16 @@ class DOMIT_Lite_TextNode extends DOMIT_Lite_Node { * DOM Text Node constructor * @param string The text of the node */ - function DOMIT_Lite_TextNode($data) { + function DOMIT_Lite_TextNode($data) { $this->_constructor(); $this->nodeType = DOMIT_TEXT_NODE; $this->nodeName = '#text'; - $this->setText($data); - } //DOMIT_Lite_TextNode + $this->setText($data); + } //DOMIT_Lite_TextNode + + function __construct($data) { + $this->DOMIT_Lite_TextNode($data); + } /** * Returns the text contained in the current node @@ -1591,12 +1602,16 @@ class DOMIT_Lite_CDATASection extends DOMIT_Lite_TextNode { * DOM CDATA Section node constructor * @param string The text of the node */ - function DOMIT_Lite_CDATASection($data) { + function DOMIT_Lite_CDATASection($data) { $this->_constructor(); $this->nodeType = DOMIT_CDATA_SECTION_NODE; $this->nodeName = '#cdata-section'; - $this->setText($data); - } //DOMIT_Lite_CDATASection + $this->setText($data); + } //DOMIT_Lite_CDATASection + + function __construct($data) { + $this->DOMIT_Lite_CDATASection($data); + } /** * Generates a string representation of the node and its children @@ -1733,7 +1748,7 @@ function dumpTextNode() { * @param string The tag name of the current element * @param Array An array of the element attributes */ - function startElement(&$parser, $name, $attrs) { + function startElement($parser, $name, $attrs) { if ($this->inTextNode) { $this->dumpTextNode(); } @@ -1749,7 +1764,7 @@ function startElement(&$parser, $name, $attrs) { * @param Object A reference to the current SAX parser * @param string The tag name of the current element */ - function endElement(&$parser, $name) { + function endElement($parser, $name) { if ($this->inTextNode) { $this->dumpTextNode(); } @@ -1762,7 +1777,7 @@ function endElement(&$parser, $name) { * @param Object A reference to the current SAX parser * @param string The current text data */ - function dataElement(&$parser, $data) { + function dataElement($parser, $data) { if (!$this->inCDATASection) $this->inTextNode = true; $this->parseContainer .= $data; @@ -1773,7 +1788,7 @@ function dataElement(&$parser, $data) { * @param Object A reference to the current SAX parser * @param string The current text data */ - function cdataElement(&$parser, $data) { + function cdataElement($parser, $data) { $currentNode =& $this->xmlDoc->createCDATASection($data); $this->lastChild->appendChild($currentNode); @@ -1784,7 +1799,7 @@ function cdataElement(&$parser, $data) { * @param Object A reference to the current SAX parser * @param string The current data */ - function defaultDataElement(&$parser, $data) { + function defaultDataElement($parser, $data) { if (strlen($data) > 2){ $pre = strtoupper(substr($data, 0, 3)); @@ -1811,4 +1826,4 @@ function defaultDataElement(&$parser, $data) { } //defaultDataElement } //DOMIT_Lite_Parser -?> \ No newline at end of file +?> diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_shared.php b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_shared.php index 58f522bf9..5a6e6662d 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_shared.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_shared.php @@ -94,7 +94,7 @@ * @package domit-xmlparser * @author John Heinstein */ -class UIDGenerator { +class UIDGenerator { /** @var int A seed value for generating uids */ var $seed; /** @var int A tally of the number of uids generated */ @@ -103,9 +103,13 @@ class UIDGenerator { /** * UIDGenerator constructor */ - function UIDGenerator() { - $this->seed = 'node' . time(); - } //UIDGenerator + function UIDGenerator() { + $this->seed = 'node' . time(); + } //UIDGenerator + + function __construct() { + $this->UIDGenerator(); + } /** * Generates a unique id @@ -129,7 +133,7 @@ class DOMIT_DOMException { * @param int The error number * @param string A string explanation of the error */ - function raiseException($errorNum, $errorString) { + public static function raiseException($errorNum, $errorString) { $errorMessage = 'Error: ' . $errorNum . "\n " . $errorString; if ((!isset($GLOBALS['DOMIT_ERROR_FORMATTING_HTML'])) || @@ -192,4 +196,4 @@ function &createDocumentType($qualifiedName, $publicID, $systemID) { } //createDocumentType } //DOMIT_DOMImplementation -?> \ No newline at end of file +?> diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_utilities.php b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_utilities.php index baf52e6bb..9f834e9d5 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_domit_utilities.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_domit_utilities.php @@ -38,7 +38,7 @@ function DOMIT_Utilities() { * @param boolean True if illegal xml characters in text nodes and attributes should be converted to entities * @return string The formatted string representation */ - function toNormalizedString (&$node, $subEntities=false, $definedEntities) { + public static function toNormalizedString (&$node, $subEntities=false, $definedEntities=array()) { $node_level = 0; $response = ''; @@ -65,7 +65,7 @@ function toNormalizedString (&$node, $subEntities=false, $definedEntities) { * @param array User defined translation table for entities * @return string The formatted text */ - function convertEntities($text, $definedEntities) { + public static function convertEntities($text, $definedEntities) { global $DOMIT_PREDEFINED_ENTITIES; $result = strtr($text, $DOMIT_PREDEFINED_ENTITIES); $result = strtr($result, $definedEntities); @@ -80,7 +80,7 @@ function convertEntities($text, $definedEntities) { * @param array User defined translation table for entities * @return string The normalized string representation */ - function getNormalizedString(&$node, $node_level, $subEntities=false, $definedEntities) { + public static function getNormalizedString(&$node, $node_level, $subEntities=false, $definedEntities=array()) { $response = ''; switch ($node->nodeType) { @@ -160,7 +160,7 @@ function getNormalizedString(&$node, $node_level, $subEntities=false, $definedEn * @param array User defined translation table for entities * @return string The normalized string representation */ - function getNormalizedElementString(&$node, $response, $node_level, + public static function getNormalizedElementString(&$node, $response, $node_level, $subEntities, $definedEntities) { $response .= '<' . $node->nodeName; @@ -240,7 +240,7 @@ function getNormalizedElementString(&$node, $response, $node_level, * @param Object The node to be tested * @return boolean True if the node is a Text node */ - function isTextNode(&$node) { + public static function isTextNode(&$node) { $type = $node->nodeType; return (($type == DOMIT_TEXT_NODE) || ($type == DOMIT_CDATA_SECTION_NODE)); } //isTextNode @@ -250,7 +250,7 @@ function isTextNode(&$node) { * @param int The current node level * @return string The indentation required for the specified node level */ - function getIndentation($node_level) { + public static function getIndentation($node_level) { $INDENT_LEN = ' '; $indentation = "\n"; @@ -266,7 +266,7 @@ function getIndentation($node_level) { * @param string The file name * @return string The file name, stripped of its extension */ - function removeExtension($fileName) { + public static function removeExtension($fileName) { $total = strlen($fileName); $index = -1; @@ -288,7 +288,7 @@ function removeExtension($fileName) { * @param string The XML text * @return boolean True if the XML text is valid */ - function validateXML($xmlText) { + public static function validateXML($xmlText) { //this does only rudimentary validation //at this point in time $isValid = true; @@ -313,7 +313,7 @@ function validateXML($xmlText) { * Set the browser header to interpret data as UTF-8 formatted * @param string The content type of the data */ - function printUTF8Header($contentType = 'text/html') { + public static function printUTF8Header($contentType = 'text/html') { echo header('Content-type: ' . $contentType . '; charset=utf-8'); } //printUTF8Header @@ -323,7 +323,7 @@ function printUTF8Header($contentType = 'text/html') { * @param boolean True if the string is to be sent directly to output * @return string The HTML formatted string */ - function forHTML($text, $doPrint = false) { + public static function forHTML($text, $doPrint = false) { if ($doPrint) { print ('
' . htmlspecialchars($text) . '
'); } @@ -337,7 +337,7 @@ function forHTML($text, $doPrint = false) { * @param object The document or node to which the child nodes should be appended * @param array An associative multidimensional array of elements and values */ - function fromArray (&$node, &$myArray) { + public static function fromArray (&$node, &$myArray) { if ($node->nodeType == DOMIT_DOCUMENT_NODE) { $docNode =& $node; } @@ -369,8 +369,8 @@ function fromArray (&$node, &$myArray) { } } //fromArray - function parseAttributes() { + public static function parseAttributes() { } //parseAttributes } //DOMIT_Utilities -?> \ No newline at end of file +?> diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_lite_parser.php b/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_lite_parser.php index 936c88369..45f2467ba 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_lite_parser.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_lite_parser.php @@ -36,10 +36,14 @@ class SAXY_Lite_Parser extends SAXY_Parser_Base { /** * Constructor for SAX parser */ - function SAXY_Lite_Parser() { - $this->SAXY_Parser_Base(); - $this->state = SAXY_STATE_NONE; - } //SAXY_Lite_Parser + function SAXY_Lite_Parser() { + $this->SAXY_Parser_Base(); + $this->state = SAXY_STATE_NONE; + } //SAXY_Lite_Parser + + function __construct() { + $this->SAXY_Lite_Parser(); + } /** * Returns the current version of SAXY Lite diff --git a/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_shared.php b/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_shared.php index 36b0cdcfb..7421c3da6 100644 --- a/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_shared.php +++ b/htdocs/applications/scielo-org/classes/domit-1/xml_saxy_shared.php @@ -56,9 +56,13 @@ class SAXY_Parser_Base { /** * Constructor for SAX parser */ - function SAXY_Parser_Base() { - $this->charContainer = ''; - } //SAXY_Parser_Base + function SAXY_Parser_Base() { + $this->charContainer = ''; + } //SAXY_Parser_Base + + function __construct() { + $this->SAXY_Parser_Base(); + } /** * Sets a reference to the handler for the start element event diff --git a/htdocs/applications/scielo-org/classes/services/AccessServiceBar.php b/htdocs/applications/scielo-org/classes/services/AccessServiceBar.php index 38cd93f8b..9b978e46a 100644 --- a/htdocs/applications/scielo-org/classes/services/AccessServiceBar.php +++ b/htdocs/applications/scielo-org/classes/services/AccessServiceBar.php @@ -44,7 +44,13 @@ function getStats(){ } function buildGraphic($stats){ - $stat = $stats->getRequests(); + $stat = is_object($stats) && method_exists($stats, 'getRequests') + ? $stats->getRequests() + : array(); + if (!is_array($stat) && !($stat instanceof Traversable)) { + $stat = array(); + } + $values = array(); foreach ($stat as $s){ $mes = intval($s->getMonth()); $ano = intval($s->getYear()); @@ -173,12 +179,20 @@ function buildGraphic($stats){ * @return int[] $anos * **/ - function getYears($stats) - { - // Recebe os dados estatisticos de um artigo - $stat = $stats->getRequests(); - $ano = array(); - + function getYears($stats) + { + // Recebe os dados estatisticos de um artigo + $stat = is_object($stats) && method_exists($stats, 'getRequests') + ? $stats->getRequests() + : array(); + if (!is_array($stat) && !($stat instanceof Traversable)) { + $stat = array(); + } + $ano = array(); + if (!is_array($stat)) { + return $ano; + } + for($i = 0, $j = 0; $i getRequests(); + $stat = is_object($stats) && method_exists($stats, 'getRequests') + ? $stats->getRequests() + : array(); + if (!is_array($stat) && !($stat instanceof Traversable)) { + $stat = array(); + } + $values = array(); foreach ($stat as $s) { @@ -262,20 +282,24 @@ function buildGraphicByYearFlash($stats, $startYear, $lastYear) e o php retorna o array quebrado Ex: ([0] => 5, [1] => 7, [5] => 6) pulando alguns elementos */ - if(is_null($values[$startYear + $i][$j+1])) + $year = $startYear + $i; + if(!isset($values[$year]) || !array_key_exists($j + 1, $values[$year]) + || is_null($values[$year][$j + 1])) $dadosRequeridos[$i][$j] = 'null'; // Para o Flash entender que aqui é NULL else - $dadosRequeridos[$i][$j] = $values[$startYear + $i][$j+1]; + $dadosRequeridos[$i][$j] = $values[$year][$j + 1]; } // Procuramos o maior valor daquele ano - $tempMax = max($values[$startYear + $i]); + $tempMax = isset($values[$year]) && count($values[$year]) > 0 + ? max($values[$year]) + : 0; if($tempMax > $maximo) $maximo = $tempMax; // Criamos a linha de um determinado ano $graph->set_data( $dadosRequeridos[$i] ); - $graph->line_hollow( 2, 3,$cores[$i], ($startYear + $i) , 11 ); + $graph->line_hollow( 2, 3,$cores[$i % count($cores)], $year, 11 ); $graph->set_tool_tip( ACCESSES.': #val#' ); } diff --git a/htdocs/applications/scielo-org/classes/services/Service.php b/htdocs/applications/scielo-org/classes/services/Service.php index fc02edaac..92432f107 100644 --- a/htdocs/applications/scielo-org/classes/services/Service.php +++ b/htdocs/applications/scielo-org/classes/services/Service.php @@ -31,10 +31,9 @@ function setResultInXML($call){ function getResultInXML(){ return $this->_callResult; } - function callService($url){ - $fp = fopen ($url,"r"); - print ''; - if ($fp) { + function callService($url){ + $fp = fopen ($url,"r"); + if ($fp) { $xml = ""; do { $data = fread($fp, 1024); diff --git a/htdocs/applications/scielo-org/pages/services/articleRequestGraphic.php b/htdocs/applications/scielo-org/pages/services/articleRequestGraphic.php index d7b393ab5..fea74a721 100644 --- a/htdocs/applications/scielo-org/pages/services/articleRequestGraphic.php +++ b/htdocs/applications/scielo-org/pages/services/articleRequestGraphic.php @@ -6,18 +6,34 @@ -setParam('pid',$_REQUEST['pid']); - $accessService->setParam('app',$scielomaindef["SITE_INFO"][APP_NAME]); - $startYear = $_REQUEST['startYear']; - $lastYear = $_REQUEST['lastYear']; + $scielomaindef = parse_ini_file($DirNameLocalGraphPage."/../../../../scielo.def.php", true); + require_once(dirname(__FILE__)."/../../classes/services/AccessServiceBar.php"); + $pid = scielo_validate_article_pid(isset($_GET['pid']) ? $_GET['pid'] : ''); + if ($pid === false) { + http_response_code(400); + exit('Invalid request'); + } + $accessService = new AccessService(); + $accessService->setParam('pid', $pid); + $accessService->setParam('app', $scielomaindef["SITE_INFO"]['APP_NAME']); + $startYear = isset($_GET['startYear']) && preg_match('/^(?:19|20)[0-9]{2}$/D', $_GET['startYear']) + ? (int) $_GET['startYear'] + : null; + $lastYear = isset($_GET['lastYear']) && preg_match('/^(?:19|20)[0-9]{2}$/D', $_GET['lastYear']) + ? (int) $_GET['lastYear'] + : null; + if ($startYear !== null && $lastYear !== null && $startYear > $lastYear) { + $tmpYear = $startYear; + $startYear = $lastYear; + $lastYear = $tmpYear; + } if($startYear !="" && $lastYear!="") { $mensagem = $accessService->buildGraphicByYearFlash($accessService->getStats(), $startYear, $lastYear); diff --git a/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php b/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php index a6cbdc6cc..a03f53fac 100644 --- a/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php +++ b/htdocs/applications/scielo-org/pages/services/articleRequestGraphicPage.php @@ -12,10 +12,15 @@ $DirHtml = $DirNameLocalGraphPage."../html/" .$lang . "/"; $site = parse_ini_file($DirNameLocalGraphPage."/../../../../ini/" . $lang . "/bvs.ini", true); - $scielodef = parse_ini_file($DirNameLocalGraphPage."/../../scielo.def.php", true); - $scielomaindef = parse_ini_file($DirNameLocalGraphPage."/../../../../scielo.def.php", true); - - $pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : ''; + $scielodef = parse_ini_file($DirNameLocalGraphPage."/../../scielo.def.php", true); + $scielomaindef = parse_ini_file($DirNameLocalGraphPage."/../../../../scielo.def.php", true); + + $lang = scielo_validate_language($lang, 'pt'); + $pid = scielo_validate_article_pid(isset($_GET['pid']) ? $_GET['pid'] : ''); + if ($pid === false) { + http_response_code(400); + exit('Invalid request'); + } $requestedCaller = isset($_REQUEST['caller']) ? $_REQUEST['caller'] : ''; $caller = scielo_internal_host_from_config($scielomaindef); if ($requestedCaller !== '' && $requestedCaller !== $caller) { @@ -32,16 +37,27 @@ $articleService->setParams($pid); $article = $articleService->getArticle(); - $accessService = new AccessService(); - $accessService->setParam('pid',$_REQUEST['pid']); - $accessService->setParam('app',$scielomaindef["SITE_INFO"][APP_NAME]); - $years = array(); - $years = $accessService->getYears($accessService->getStats()); - - $yearsLastIndex = (count($years) - 1); - - $startYear = $_REQUEST['startYear'] ? $_REQUEST['startYear'] : $years[$yearsLastIndex]; - $lastYear = $_REQUEST['lastYear'] ? $_REQUEST['lastYear'] : $years[$yearsLastIndex]; + $accessService = new AccessService(); + $accessService->setParam('pid', $pid); + $accessService->setParam('app', $scielomaindef["SITE_INFO"]['APP_NAME']); + $years = array(); + $years = $accessService->getYears($accessService->getStats()); + $years = array_values(array_filter(array_map('intval', $years), function ($year) { + return $year >= 1900 && $year <= ((int) date('Y') + 1); + })); + + $defaultYear = count($years) > 0 ? $years[count($years) - 1] : (int) date('Y'); + $startYear = isset($_GET['startYear']) && preg_match('/^(?:19|20)[0-9]{2}$/D', $_GET['startYear']) + ? (int) $_GET['startYear'] + : $defaultYear; + $lastYear = isset($_GET['lastYear']) && preg_match('/^(?:19|20)[0-9]{2}$/D', $_GET['lastYear']) + ? (int) $_GET['lastYear'] + : $defaultYear; + if ($startYear > $lastYear) { + $tmpYear = $startYear; + $startYear = $lastYear; + $lastYear = $tmpYear; + } ?> @@ -52,17 +68,17 @@ - - - - - + + + + + - - - + + +
@@ -81,12 +97,12 @@ $pos = strrpos($author, ";"); $author[$pos] = " "; - echo $author; - echo ''; - echo getTitle($article->getTitle(), $lang).". "; - echo (''); - echo scielo_utf8_encode($article->getSerial(). ', '.$article->getYear().', vol.'.$article->getVolume()); - echo scielo_utf8_encode(', n. '.$article->getNumber().', ISSN '.substr($article->getPID(),1,9).'.

'."\n"); + echo scielo_escape_html($author); + echo ''; + echo scielo_escape_html(getTitle($article->getTitle(), $lang)).". "; + echo (''); + echo scielo_escape_html(scielo_utf8_encode($article->getSerial(). ', '.$article->getYear().', vol.'.$article->getVolume())); + echo scielo_escape_html(scielo_utf8_encode(', n. '.$article->getNumber().', ISSN '.substr($article->getPID(),1,9).'.')).'

'."\n"; ?> @@ -101,9 +117,9 @@ '.$years[$i].''; + echo ''; } - echo ''; + echo ''; ?> @@ -111,27 +127,29 @@ '.$years[$i].''; + echo ''; } - echo ''; + echo ''; ?> - - - + +
'; - echo "
".ARTICLE_ACCESS."
"; - ?> -
diff --git a/htdocs/applications/scielo-org/pages/services/sendMail.php b/htdocs/applications/scielo-org/pages/services/sendMail.php index e94bf51f4..45b032ed7 100644 --- a/htdocs/applications/scielo-org/pages/services/sendMail.php +++ b/htdocs/applications/scielo-org/pages/services/sendMail.php @@ -1,11 +1,14 @@ - 'caller_override_ignored') ); } - -//geting metadatas from PID + +if ($acao === 'send') { + if ($requestMethod !== 'POST') { + header('Allow: POST'); + http_response_code(405); + exit('Method not allowed'); + } + + if (!scielo_csrf_token_is_valid(isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '')) { + scielo_audit_event('access.denied', 'unauthorized', 'service.article_email', $pid, array( + 'reason' => 'invalid_csrf_token', + )); + http_response_code(403); + exit('Request denied'); + } + + $fromEmail = scielo_validate_email_address(isset($_POST['from']) ? $_POST['from'] : ''); + $toEmail = scielo_validate_email_address(isset($_POST['to']) ? $_POST['to'] : ''); + $fromName = scielo_validate_person_name(isset($_POST['fromName']) ? $_POST['fromName'] : ''); + $toName = scielo_validate_person_name(isset($_POST['toName']) ? $_POST['toName'] : ''); + $comment = scielo_validate_comment(isset($_POST['comment']) ? $_POST['comment'] : ''); + if ($fromEmail === false || $toEmail === false || $fromName === false + || $toName === false || $comment === false) { + scielo_audit_event('article.email_send', 'failure', 'article', $pid, array( + 'reason' => 'invalid_input', + )); + http_response_code(400); + exit('Invalid request'); + } + + $rateLimit = scielo_rate_limit('article.email_send', 5, 3600); + if (!$rateLimit['allowed']) { + scielo_audit_event('access.denied', 'unauthorized', 'service.article_email', $pid, array( + 'reason' => 'rate_limit_exceeded', + 'retry_after' => $rateLimit['retry_after'], + )); + header('Retry-After: ' . $rateLimit['retry_after']); + http_response_code(429); + exit('Too many requests'); + } +} + +//geting metadatas from PID $articleService = new ArticleService($caller); $articleService->setParams($pid); $article = $articleService->getArticle(); -switch($acao){ - case "send": - $articleURL = 'http://'.$caller.'/scielo.php?script=sci_abstract&pid='.$pid.'&lng=en&nrm=iso&tlng=en'; - $link = ''.getTitle($article->getTitle()).''; - - $msg = file_get_contents(dirname(__FILE__)."/../../html/".$lang."/send_mail_msg.html"); - - $msg = str_replace("[toName]",$cgi["toName"],$msg); - $msg = str_replace("[fromName]",$cgi["fromName"],$msg); - $msg = str_replace("[serialName]",$article->getSerial(),$msg); - $msg = str_replace("[articleTitleURL]",$link,$msg); - $msg = str_replace("[articleURL]",$articleURL,$msg); - $msg = str_replace("[commentary]",$cgi["comment"],$msg); - - - $search = array ('@]*?>.*?@si', // Strip out javascript - '@<[\/\!]*?[^<>]*?>@si', // Strip out HTML tags - '@([\r\n])[\s]+@', // Strip out white space - '@&(quot|#34);@i', // Replace HTML entities - '@&(amp|#38);@i', - '@&(lt|#60);@i', - '@&(gt|#62);@i', - '@&(nbsp|#160);@i', - '@&(iexcl|#161);@i', - '@&(cent|#162);@i', - '@&(pound|#163);@i', - '@&(copy|#169);@i', - '@&#(\d+);@e'); // evaluate as php - - $replace = array ('', - '', - '\1', - '"', - '&', - '<', - '>', - ' ', - chr(161), - chr(162), - chr(163), - chr(169), - 'chr(\1)'); - - $msg_no_html = preg_replace($search, $replace, $msg); - - - $_mail = new PHPMailer(); +switch($acao){ + case "send": + $articleURL = 'http://' . $caller . '/scielo.php?' . http_build_query(array( + 'script' => 'sci_abstract', + 'pid' => $pid, + 'lng' => 'en', + 'nrm' => 'iso', + 'tlng' => 'en', + )); + $link = '' + . scielo_escape_html(getTitle($article->getTitle())) . ''; + + $msg = file_get_contents(dirname(__FILE__)."/../../html/".$lang."/send_mail_msg.html"); + + $msg = str_replace("[toName]", scielo_escape_html($toName), $msg); + $msg = str_replace("[fromName]", scielo_escape_html($fromName), $msg); + $msg = str_replace("[serialName]", scielo_escape_html($article->getSerial()), $msg); + $msg = str_replace("[articleTitleURL]",$link,$msg); + $msg = str_replace("[articleURL]", scielo_escape_html($articleURL), $msg); + $msg = str_replace("[commentary]", nl2br(scielo_escape_html($comment)), $msg); + $msg_no_html = trim(strip_tags(html_entity_decode($msg, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + + $_mail = new PHPMailer(); $_mail->isSMTP(); $_mail->SMTPAuth = true; $_mail->SMTPSecure = $mailcredentials['secure']; $_mail->Username = $mailcredentials['username']; $_mail->Password = $mailcredentials['password']; $_mail->SetLanguage('en',dirname(__FILE__) . '/../../includes/phpmailer/language/'); - $_mail->AddReplyTo($cgi["from"],$cgi["fromName"]); + $_mail->AddReplyTo($fromEmail, $fromName); $_mail->From = $mailcredentials['sender']; $_mail->FromName = "SciELO"; - $_mail->Subject = ARTICLE_SUGGESTION." ".$cgi["fromName"]; + $_mail->Subject = ARTICLE_SUGGESTION." ".$fromName; $_mail->Host = $mailcredentials['host']; $_mail->Port = $mailcredentials['port']; $_mail->Mailer = 'mail'; $_mail->IsHTML(true); $_mail->Body = $msg; $_mail->AltBody = $msg_no_html; - $_mail->AddAddress($cgi["to"], $cgi['toName']); - $send = $_mail->Send(); - if(!$send){ - $acao = "message"; - $message = $_mail->ErrorInfo; - }else{ - $acao = "message"; - $message = ARTICLE_SUBMITED_WITH_SUCCESS; - } + $_mail->AddAddress($toEmail, $toName); + $send = $_mail->Send(); + if(!$send){ + $acao = "message"; + $message = 'Unable to send the message.'; + scielo_audit_event('article.email_send', 'failure', 'article', $pid, array( + 'reason' => 'mailer_failure', + )); + }else{ + $acao = "message"; + $message = ARTICLE_SUBMITED_WITH_SUCCESS; + scielo_audit_event('article.email_send', 'success', 'article', $pid); + } break; } ?> @@ -121,9 +150,9 @@ - - - <?= $site['title']?> + <head> + <title> + <?=scielo_escape_html($site['title'])?> @@ -142,13 +171,7 @@
- -
+

@@ -158,19 +181,20 @@ -
+
- - - + + + + - + @@ -183,17 +202,17 @@ function x($s, $TAG){

  • - +
  • - +
  • - +
  • @@ -211,12 +230,12 @@ function x($s, $TAG){

    • - +
    • - +
    • diff --git a/tests/security_regression.php b/tests/security_regression.php index b46d05231..6fdccb17d 100644 --- a/tests/security_regression.php +++ b/tests/security_regression.php @@ -9,12 +9,25 @@ function assert_true($condition, $message) } require_once(__DIR__ . '/../htdocs/security.php'); +require_once(__DIR__ . '/../htdocs/applications/scielo-org/classes/domit-1/xml_domit_lite_include.inc.php'); + +$domitDocument = new DOMIT_Lite_Document(); +assert_true($domitDocument->parseXML('
      Safe title
      ') === true, 'DOMIT should parse XML on PHP 8.5'); +$domitTitles = $domitDocument->getElementsByPath('//TITLE'); +assert_true($domitTitles->item(0)->getText() === 'Safe title', 'DOMIT should expose parsed XML content'); assert_true(scielo_validate_internal_host('127.0.0.1') === '127.0.0.1', 'IPv4 host should be accepted'); assert_true(scielo_validate_internal_host('backend.example.org:8080') === 'backend.example.org:8080', 'hostname and port should be accepted'); assert_true(scielo_validate_internal_host('attacker.example/path') === false, 'path must not be accepted as a host'); assert_true(scielo_validate_internal_host('user@attacker.example') === false, 'userinfo must not be accepted as a host'); assert_true(scielo_validate_internal_host('backend.example.org:65536') === false, 'invalid port must be rejected'); +assert_true(scielo_validate_article_pid('S0100-879X2013000100058') === 'S0100-879X2013000100058', 'valid article PID should be accepted'); +assert_true(scielo_validate_article_pid('S0100-879X
getTitle());?>getTitle()));?>
diff --git a/htdocs/security.php b/htdocs/security.php index 55947e83b..15795a702 100644 --- a/htdocs/security.php +++ b/htdocs/security.php @@ -68,6 +68,154 @@ function scielo_audit_event($action, $result, $resource, $resourceId = null, $ex error_log(json_encode($entry, JSON_UNESCAPED_SLASHES)); } +function scielo_escape_html($value) +{ + return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} + +function scielo_validate_language($value, $default = 'en') +{ + $allowed = array('pt', 'en', 'es'); + return in_array($value, $allowed, true) ? $value : $default; +} + +function scielo_validate_article_pid($value) +{ + if (!is_string($value) || strlen($value) > 64) { + return false; + } + + return preg_match('/^S[0-9]{4}-[0-9]{3}[0-9Xx][A-Za-z0-9.()_-]{1,40}$/D', $value) + ? $value + : false; +} + +function scielo_validate_email_address($value) +{ + if (!is_string($value) || strlen($value) > 254 || preg_match('/[\r\n]/', $value)) { + return false; + } + + $value = trim($value); + return filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : false; +} + +function scielo_validate_person_name($value) +{ + if (!is_string($value)) { + return false; + } + + $value = trim($value); + if (!preg_match('/^[\p{L}\p{M}\p{N} .,\x27-]{1,100}$/u', $value)) { + return false; + } + + return $value; +} + +function scielo_validate_comment($value) +{ + if (!is_string($value) || strlen($value) > 1000 + || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value)) { + return false; + } + + return trim($value); +} + +function scielo_start_secure_session() +{ + if (session_status() === PHP_SESSION_ACTIVE) { + return; + } + + session_set_cookie_params(array( + 'lifetime' => 0, + 'path' => '/', + 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'httponly' => true, + 'samesite' => 'Lax', + )); + session_start(); +} + +function scielo_csrf_token() +{ + scielo_start_secure_session(); + + if (empty($_SESSION['scielo_csrf_token'])) { + $_SESSION['scielo_csrf_token'] = bin2hex(random_bytes(32)); + } + + return $_SESSION['scielo_csrf_token']; +} + +function scielo_csrf_token_is_valid($token) +{ + scielo_start_secure_session(); + + return is_string($token) + && isset($_SESSION['scielo_csrf_token']) + && hash_equals($_SESSION['scielo_csrf_token'], $token); +} + +function scielo_rate_limit($bucket, $limit, $windowSeconds) +{ + $ipAddress = isset($_SERVER['REMOTE_ADDR']) + && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) + ? $_SERVER['REMOTE_ADDR'] + : null; + + if ($ipAddress === null || !is_string($bucket) || $bucket === '' + || $limit < 1 || $windowSeconds < 1) { + return array('allowed' => false, 'retry_after' => $windowSeconds); + } + + $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR . 'scielo-rate-limit'; + if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) { + return array('allowed' => false, 'retry_after' => $windowSeconds); + } + + $filename = $directory . DIRECTORY_SEPARATOR + . hash('sha256', $bucket . '|' . $ipAddress) . '.json'; + $handle = fopen($filename, 'c+'); + if ($handle === false || !flock($handle, LOCK_EX)) { + if (is_resource($handle)) { + fclose($handle); + } + return array('allowed' => false, 'retry_after' => $windowSeconds); + } + + $now = time(); + $contents = stream_get_contents($handle); + $timestamps = json_decode($contents, true); + if (!is_array($timestamps)) { + $timestamps = array(); + } + $timestamps = array_values(array_filter($timestamps, function ($timestamp) use ($now, $windowSeconds) { + return is_int($timestamp) && $timestamp > ($now - $windowSeconds); + })); + + $allowed = count($timestamps) < $limit; + $retryAfter = 0; + if ($allowed) { + $timestamps[] = $now; + } else { + $retryAfter = max(1, $windowSeconds - ($now - min($timestamps))); + } + + rewind($handle); + ftruncate($handle, 0); + fwrite($handle, json_encode($timestamps)); + fflush($handle); + flock($handle, LOCK_UN); + fclose($handle); + + return array('allowed' => $allowed, 'retry_after' => $retryAfter); +} + function scielo_is_loopback_request() { $remoteAddress = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; diff --git a/htdocs/statjournal.php b/htdocs/statjournal.php index 945288f4a..e8ce85e3b 100644 --- a/htdocs/statjournal.php +++ b/htdocs/statjournal.php @@ -1,7 +1,10 @@ -')>0) { +')>0) { $r = substr($s, strpos($s,'<'.$TAG.'>')); $r = substr($r, strlen('<'.$TAG.'>')); $r = substr($r, 0, strpos($r,'')); @@ -9,35 +12,51 @@ function x($s, $TAG){ $r = str_replace(']]>','',$r); } return $r; - } - $DEF = parse_ini_file("scielo.def.php", true); - - if ($_GET['lang'] != 'en' && $_GET['lang'] != 'es' && $_GET['lang'] != 'pt') $_GET['lang'] = $DEF['SITE_INFO']['STANDARD_LANG']; - - if ( strpos($DEF['SCIELO']['STAT_SERVER'],$DEF['SCIELO']['SERVER_SCIELO'])>0) { + } + $DEF = parse_ini_file("scielo.def.php", true); + + $defaultLang = isset($DEF['SITE_INFO']['STANDARD_LANG']) + ? scielo_validate_language($DEF['SITE_INFO']['STANDARD_LANG'], 'en') + : 'en'; + $lang = scielo_validate_language(isset($_GET['lang']) ? $_GET['lang'] : '', $defaultLang); + $issn = isset($_GET['issn']) && is_string($_GET['issn']) ? $_GET['issn'] : ''; + $collection = isset($_GET['collection']) && is_string($_GET['collection']) + ? $_GET['collection'] + : ''; + + if (!preg_match('/^[0-9]{4}-?[0-9]{3}[0-9Xx]$/D', $issn) + || !preg_match('/^[A-Za-z0-9._-]{1,64}$/D', $collection)) { + http_response_code(400); + exit('Invalid request'); + } + + $_GET['lang'] = $lang; + $_GET['issn'] = $issn; + $internalHost = scielo_internal_host_from_config($DEF); + $collectionEncoded = rawurlencode($collection); + + if ( strpos($DEF['SCIELO']['STAT_SERVER'],$DEF['SCIELO']['SERVER_SCIELO'])>0) { $xml = 'xml='.$DEF['SCIELO']['STAT_SERVER'].'/stat_biblio/xml/'; } else { $xml = 'no='; } - $journalInfo = file_get_contents('http://'.$DEF['SCIELO']['SERVER_SCIELO'].'/scielo.php?script=sci_serial&pid='.$_GET['issn'].'&debug=xml'); + $journalInfo = file_get_contents('http://' . $internalHost . '/scielo.php?' . http_build_query(array( + 'script' => 'sci_serial', + 'pid' => $issn, + 'debug' => 'xml', + ))); $error = x($journalInfo,'CODE'); - if ($error){ - header("Location: ".'http://'.$DEF['SCIELO']['SERVER_SCIELO'].'/scielo.php?script=sci_serial&pid='.$_GET['issn']); + if ($error){ + header("Location: ".'http://'.$DEF['SCIELO']['SERVER_SCIELO'].'/scielo.php?script=sci_serial&pid='.$_GET['issn']); + exit; } $journalInfo = x($journalInfo,'TITLEGROUP'); $j['title'] = x($journalInfo,'TITLE'); $j['acron'] = x($journalInfo,'SIGLUM'); - foreach ($_GET as $g){ - $g = str_replace('/','',$g); - $g = str_replace(':','',$g); - $g = str_replace('\\','',$g); - $g = str_replace('*','',$g); - $g = str_replace('.','',$g); - } - switch ($_GET['lang']){ + switch ($_GET['lang']){ case "en": $LABELS = array('LANG_1'=> 'i', 'LANG'=>$_GET['lang'], @@ -144,7 +163,7 @@ function x($s, $TAG){ ?> - <?php echo $LABELS['JOURNAL_TITLE']; ?> - <?php echo $LABELS['PAGE_TITLE']; ?> + <?php echo scielo_escape_html($LABELS['JOURNAL_TITLE']); ?> - <?php echo scielo_escape_html($LABELS['PAGE_TITLE']); ?> @@ -153,16 +172,16 @@ function x($s, $TAG){

- - + +

- +
- ISSN + ISSN